From 46d189fccf5f3dd0a08d2ad943cf3de08c7d6bdd Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sun, 27 Jul 2025 12:38:47 -0600 Subject: [PATCH 1/7] test(fixtures): refactor project config fixtures to enable file specification --- tests/fixtures/example_project.py | 226 +++++++++++++++++++++++------- 1 file changed, 176 insertions(+), 50 deletions(-) diff --git a/tests/fixtures/example_project.py b/tests/fixtures/example_project.py index 5d0c60886..e034cb0a4 100644 --- a/tests/fixtures/example_project.py +++ b/tests/fixtures/example_project.py @@ -3,7 +3,7 @@ import os from pathlib import Path from textwrap import dedent -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING, Generator, cast import pytest import tomlkit @@ -37,7 +37,11 @@ if TYPE_CHECKING: from typing import Any, Protocol, Sequence + from tomlkit.container import Container as TOMLContainer + from semantic_release.commit_parser import CommitParser + from semantic_release.commit_parser._base import ParserOptions + from semantic_release.commit_parser.token import ParseResult from semantic_release.hvcs import HvcsBase from semantic_release.version.version import Version @@ -53,25 +57,35 @@ class GetWheelFileFn(Protocol): def __call__(self, version_str: str) -> Path: ... class SetFlagFn(Protocol): - def __call__(self, flag: bool) -> None: ... + def __call__(self, flag: bool, toml_file: Path | str = ...) -> None: ... class UpdatePyprojectTomlFn(Protocol): - def __call__(self, setting: str, value: Any) -> None: ... + def __call__( + self, setting: str, value: Any, toml_file: Path | str = ... + ) -> None: ... class UseCustomParserFn(Protocol): - def __call__(self, module_import_str: str) -> None: ... + def __call__( + self, module_import_str: str, toml_file: Path | str = ... + ) -> None: ... class UseHvcsFn(Protocol): - def __call__(self, domain: str | None = None) -> type[HvcsBase]: ... + def __call__( + self, domain: str | None = None, toml_file: Path | str = ... + ) -> type[HvcsBase]: ... class UseParserFn(Protocol): - def __call__(self) -> type[CommitParser]: ... + def __call__( + self, toml_file: Path | str = ... + ) -> type[CommitParser[ParseResult, ParserOptions]]: ... class UseReleaseNotesTemplateFn(Protocol): - def __call__(self) -> None: ... + def __call__(self, toml_file: Path | str = ...) -> None: ... class UpdateVersionPyFileFn(Protocol): - def __call__(self, version: Version | str) -> None: ... + def __call__( + self, version: Version | str, version_file: Path | str = ... + ) -> None: ... @pytest.fixture(scope="session") @@ -300,11 +314,15 @@ def use_release_notes_template( example_project_template_dir: Path, changelog_template_dir: Path, update_pyproject_toml: UpdatePyprojectTomlFn, + pyproject_toml_file: Path, ) -> UseReleaseNotesTemplateFn: - def _use_release_notes_template() -> None: + def _use_release_notes_template( + toml_file: Path | str = pyproject_toml_file, + ) -> None: update_pyproject_toml( "tool.semantic_release.changelog.template_dir", str(changelog_template_dir), + toml_file=toml_file, ) example_project_template_dir.mkdir(parents=True, exist_ok=True) release_notes_j2 = example_project_template_dir / ".release_notes.md.j2" @@ -381,8 +399,10 @@ def example_project_template_dir( @pytest.fixture(scope="session") def update_version_py_file(version_py_file: Path) -> UpdateVersionPyFileFn: - def _update_version_py_file(version: Version | str) -> None: - cwd_version_py = version_py_file.resolve() + def _update_version_py_file( + version: Version | str, version_file: Path | str = version_py_file + ) -> None: + cwd_version_py = Path(version_file).resolve() cwd_version_py.parent.mkdir(parents=True, exist_ok=True) cwd_version_py.write_text( dedent( @@ -399,8 +419,10 @@ def _update_version_py_file(version: Version | str) -> None: def update_pyproject_toml(pyproject_toml_file: Path) -> UpdatePyprojectTomlFn: """Update the pyproject.toml file with the given content.""" - def _update_pyproject_toml(setting: str, value: Any) -> None: - cwd_pyproject_toml = pyproject_toml_file.resolve() + def _update_pyproject_toml( + setting: str, value: Any, toml_file: Path | str = pyproject_toml_file + ) -> None: + cwd_pyproject_toml = Path(toml_file).resolve() with open(cwd_pyproject_toml) as rfd: pyproject_toml = tomlkit.load(rfd) @@ -409,11 +431,13 @@ def _update_pyproject_toml(setting: str, value: Any) -> None: new_setting_key = parts.pop(-1) new_setting[new_setting_key] = value - pointer = pyproject_toml + pointer: TOMLContainer = pyproject_toml for part in parts: - if pointer.get(part, None) is None: - pointer.add(part, tomlkit.table()) - pointer = pointer.get(part, {}) + if (next_pointer := pointer.get(part, None)) is None: + next_pointer = tomlkit.table() + pointer.add(part, next_pointer) + + pointer = cast("TOMLContainer", next_pointer) if value is None: pointer.pop(new_setting_key) @@ -432,127 +456,229 @@ def pyproject_toml_config_option_parser() -> str: @pytest.fixture(scope="session") -def set_major_on_zero(update_pyproject_toml: UpdatePyprojectTomlFn) -> SetFlagFn: +def pyproject_toml_config_option_remote_type() -> str: + return f"tool.{semantic_release.__name__}.remote.type" + + +@pytest.fixture(scope="session") +def pyproject_toml_config_option_remote_domain() -> str: + return f"tool.{semantic_release.__name__}.remote.domain" + + +@pytest.fixture(scope="session") +def set_major_on_zero( + pyproject_toml_file: Path, update_pyproject_toml: UpdatePyprojectTomlFn +) -> SetFlagFn: """Turn on/off the major_on_zero setting.""" - def _set_major_on_zero(flag: bool) -> None: - update_pyproject_toml("tool.semantic_release.major_on_zero", flag) + def _set_major_on_zero( + flag: bool, toml_file: Path | str = pyproject_toml_file + ) -> None: + update_pyproject_toml("tool.semantic_release.major_on_zero", flag, toml_file) return _set_major_on_zero @pytest.fixture(scope="session") -def set_allow_zero_version(update_pyproject_toml: UpdatePyprojectTomlFn) -> SetFlagFn: +def set_allow_zero_version( + pyproject_toml_file: Path, update_pyproject_toml: UpdatePyprojectTomlFn +) -> SetFlagFn: """Turn on/off the allow_zero_version setting.""" - def _set_allow_zero_version(flag: bool) -> None: - update_pyproject_toml("tool.semantic_release.allow_zero_version", flag) + def _set_allow_zero_version( + flag: bool, toml_file: Path | str = pyproject_toml_file + ) -> None: + update_pyproject_toml( + "tool.semantic_release.allow_zero_version", flag, toml_file + ) return _set_allow_zero_version @pytest.fixture(scope="session") def use_conventional_parser( + pyproject_toml_file: Path, update_pyproject_toml: UpdatePyprojectTomlFn, pyproject_toml_config_option_parser: str, ) -> UseParserFn: """Modify the configuration file to use the Conventional parser.""" - def _use_conventional_parser() -> type[CommitParser]: - update_pyproject_toml(pyproject_toml_config_option_parser, "conventional") - return ConventionalCommitParser + def _use_conventional_parser( + toml_file: Path | str = pyproject_toml_file, + ) -> type[CommitParser[ParseResult, ParserOptions]]: + update_pyproject_toml( + pyproject_toml_config_option_parser, "conventional", toml_file=toml_file + ) + return cast( + "type[CommitParser[ParseResult, ParserOptions]]", ConventionalCommitParser + ) return _use_conventional_parser @pytest.fixture(scope="session") def use_emoji_parser( + pyproject_toml_file: Path, update_pyproject_toml: UpdatePyprojectTomlFn, pyproject_toml_config_option_parser: str, ) -> UseParserFn: """Modify the configuration file to use the Emoji parser.""" - def _use_emoji_parser() -> type[CommitParser]: - update_pyproject_toml(pyproject_toml_config_option_parser, "emoji") - return EmojiCommitParser + def _use_emoji_parser( + toml_file: Path | str = pyproject_toml_file, + ) -> type[CommitParser[ParseResult, ParserOptions]]: + update_pyproject_toml( + pyproject_toml_config_option_parser, "emoji", toml_file=toml_file + ) + return cast("type[CommitParser[ParseResult, ParserOptions]]", EmojiCommitParser) return _use_emoji_parser @pytest.fixture(scope="session") def use_scipy_parser( + pyproject_toml_file: Path, update_pyproject_toml: UpdatePyprojectTomlFn, pyproject_toml_config_option_parser: str, ) -> UseParserFn: """Modify the configuration file to use the Scipy parser.""" - def _use_scipy_parser() -> type[CommitParser]: - update_pyproject_toml(pyproject_toml_config_option_parser, "scipy") - return ScipyCommitParser + def _use_scipy_parser( + toml_file: Path | str = pyproject_toml_file, + ) -> type[CommitParser[ParseResult, ParserOptions]]: + update_pyproject_toml( + pyproject_toml_config_option_parser, "scipy", toml_file=toml_file + ) + return cast("type[CommitParser[ParseResult, ParserOptions]]", ScipyCommitParser) return _use_scipy_parser @pytest.fixture(scope="session") def use_custom_parser( + pyproject_toml_file: Path, update_pyproject_toml: UpdatePyprojectTomlFn, pyproject_toml_config_option_parser: str, ) -> UseCustomParserFn: """Modify the configuration file to use a user defined string parser.""" - def _use_custom_parser(module_import_str: str) -> None: - update_pyproject_toml(pyproject_toml_config_option_parser, module_import_str) + def _use_custom_parser( + module_import_str: str, toml_file: Path | str = pyproject_toml_file + ) -> None: + update_pyproject_toml( + pyproject_toml_config_option_parser, module_import_str, toml_file=toml_file + ) return _use_custom_parser @pytest.fixture(scope="session") -def use_github_hvcs(update_pyproject_toml: UpdatePyprojectTomlFn) -> UseHvcsFn: +def use_github_hvcs( + pyproject_toml_file: Path, + update_pyproject_toml: UpdatePyprojectTomlFn, + pyproject_toml_config_option_remote_type: str, + pyproject_toml_config_option_remote_domain: str, +) -> UseHvcsFn: """Modify the configuration file to use GitHub as the HVCS.""" - def _use_github_hvcs(domain: str | None = None) -> type[HvcsBase]: - update_pyproject_toml("tool.semantic_release.remote.type", "github") + def _use_github_hvcs( + domain: str | None = None, toml_file: Path | str = pyproject_toml_file + ) -> type[HvcsBase]: + update_pyproject_toml( + pyproject_toml_config_option_remote_type, + Github.__name__.lower(), + toml_file=toml_file, + ) + if domain is not None: - update_pyproject_toml("tool.semantic_release.remote.domain", domain) + update_pyproject_toml( + pyproject_toml_config_option_remote_domain, domain, toml_file=toml_file + ) + return Github return _use_github_hvcs @pytest.fixture(scope="session") -def use_gitlab_hvcs(update_pyproject_toml: UpdatePyprojectTomlFn) -> UseHvcsFn: +def use_gitlab_hvcs( + pyproject_toml_file: Path, + update_pyproject_toml: UpdatePyprojectTomlFn, + pyproject_toml_config_option_remote_type: str, + pyproject_toml_config_option_remote_domain: str, +) -> UseHvcsFn: """Modify the configuration file to use GitLab as the HVCS.""" - def _use_gitlab_hvcs(domain: str | None = None) -> type[HvcsBase]: - update_pyproject_toml("tool.semantic_release.remote.type", "gitlab") + def _use_gitlab_hvcs( + domain: str | None = None, toml_file: Path | str = pyproject_toml_file + ) -> type[HvcsBase]: + update_pyproject_toml( + pyproject_toml_config_option_remote_type, + Gitlab.__name__.lower(), + toml_file=toml_file, + ) + if domain is not None: - update_pyproject_toml("tool.semantic_release.remote.domain", domain) + update_pyproject_toml( + pyproject_toml_config_option_remote_domain, domain, toml_file=toml_file + ) + return Gitlab return _use_gitlab_hvcs @pytest.fixture(scope="session") -def use_gitea_hvcs(update_pyproject_toml: UpdatePyprojectTomlFn) -> UseHvcsFn: +def use_gitea_hvcs( + pyproject_toml_file: Path, + update_pyproject_toml: UpdatePyprojectTomlFn, + pyproject_toml_config_option_remote_type: str, + pyproject_toml_config_option_remote_domain: str, +) -> UseHvcsFn: """Modify the configuration file to use Gitea as the HVCS.""" - def _use_gitea_hvcs(domain: str | None = None) -> type[HvcsBase]: - update_pyproject_toml("tool.semantic_release.remote.type", "gitea") + def _use_gitea_hvcs( + domain: str | None = None, toml_file: Path | str = pyproject_toml_file + ) -> type[HvcsBase]: + update_pyproject_toml( + pyproject_toml_config_option_remote_type, + Gitea.__name__.lower(), + toml_file=toml_file, + ) + if domain is not None: - update_pyproject_toml("tool.semantic_release.remote.domain", domain) + update_pyproject_toml( + pyproject_toml_config_option_remote_domain, domain, toml_file=toml_file + ) + return Gitea return _use_gitea_hvcs @pytest.fixture(scope="session") -def use_bitbucket_hvcs(update_pyproject_toml: UpdatePyprojectTomlFn) -> UseHvcsFn: +def use_bitbucket_hvcs( + pyproject_toml_file: Path, + update_pyproject_toml: UpdatePyprojectTomlFn, + pyproject_toml_config_option_remote_type: str, + pyproject_toml_config_option_remote_domain: str, +) -> UseHvcsFn: """Modify the configuration file to use BitBucket as the HVCS.""" - def _use_bitbucket_hvcs(domain: str | None = None) -> type[HvcsBase]: - update_pyproject_toml("tool.semantic_release.remote.type", "bitbucket") + def _use_bitbucket_hvcs( + domain: str | None = None, toml_file: Path | str = pyproject_toml_file + ) -> type[HvcsBase]: + update_pyproject_toml( + pyproject_toml_config_option_remote_type, + Bitbucket.__name__.lower(), + toml_file=toml_file, + ) + if domain is not None: - update_pyproject_toml("tool.semantic_release.remote.domain", domain) + update_pyproject_toml( + pyproject_toml_config_option_remote_domain, domain, toml_file=toml_file + ) + return Bitbucket return _use_bitbucket_hvcs From 4ab60ca4fbeacedb0f2c667165b795b022a390de Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Tue, 5 Aug 2025 00:50:26 -0600 Subject: [PATCH 2/7] test(fixtures): refactor e2e infrastructure to be more configurable & resilient --- tests/conftest.py | 33 +- tests/e2e/cmd_changelog/test_changelog.py | 26 +- .../e2e/cmd_version/bump_version/conftest.py | 34 +- tests/e2e/conftest.py | 27 +- tests/e2e/test_main.py | 6 +- tests/fixtures/example_project.py | 114 ++- tests/fixtures/git_repo.py | 656 ++++++++++++------ tests/unit/semantic_release/cli/test_util.py | 27 +- tests/util.py | 12 +- 9 files changed, 658 insertions(+), 277 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2d081f62b..7a00b8dad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -"""Note: fixtures are stored in the tests/fixtures directory for better organisation""" +"""Note: fixtures are stored in the tests/fixtures directory for better organization""" from __future__ import annotations @@ -9,7 +9,7 @@ from hashlib import md5 from pathlib import Path from tempfile import NamedTemporaryFile -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from unittest import mock import pytest @@ -17,13 +17,15 @@ from filelock import FileLock from git import Commit, Repo +from semantic_release.version.version import Version + from tests.const import PROJ_DIR from tests.fixtures import * from tests.util import copy_dir_tree, remove_dir_tree if TYPE_CHECKING: from tempfile import _TemporaryFileWrapper - from typing import Any, Callable, Generator, Protocol, Sequence, TypedDict + from typing import Any, Callable, Generator, Optional, Protocol, Sequence, TypedDict from click.testing import Result from filelock import AcquireReturnProxy @@ -325,7 +327,7 @@ def _get_authorization_to_build_repo_cache( def get_cached_repo_data(request: pytest.FixtureRequest) -> GetCachedRepoDataFn: def _get_cached_repo_data(proj_dirname: str) -> RepoData | None: cache_key = f"psr/repos/{proj_dirname}" - return request.config.cache.get(cache_key, None) + return cast("Optional[RepoData]", request.config.cache.get(cache_key, None)) return _get_cached_repo_data @@ -335,6 +337,10 @@ def set_cached_repo_data(request: pytest.FixtureRequest) -> SetCachedRepoDataFn: def magic_serializer(obj: Any) -> Any: if isinstance(obj, Path): return obj.__fspath__() + + if isinstance(obj, Version): + return obj.__dict__ + return obj def _set_cached_repo_data(proj_dirname: str, data: RepoData) -> None: @@ -386,13 +392,30 @@ def _build_repo_w_cache_checking( with log_file_lock, log_file.open(mode="a") as afd: afd.write(f"{stable_now_date().isoformat()}: {build_msg}...\n") + try: + # Try to build repository but catch any errors so that it doesn't cascade through all tests + # do to an unreleased lock + build_definition = build_repo_func(cached_repo_path) + except Exception: + remove_dir_tree(cached_repo_path, force=True) + + if filelock: + filelock.lock.release() + + with log_file_lock, log_file.open(mode="a") as afd: + afd.write( + f"{stable_now_date().isoformat()}: {build_msg}...FAILED\n" + ) + + raise + # Marks the date when the cached repo was created set_cached_repo_data( repo_name, { "build_date": today_date_str, "build_spec_hash": build_spec_hash, - "build_definition": build_repo_func(cached_repo_path), + "build_definition": build_definition, }, ) diff --git a/tests/e2e/cmd_changelog/test_changelog.py b/tests/e2e/cmd_changelog/test_changelog.py index edc2a8c63..3f3bb56da 100644 --- a/tests/e2e/cmd_changelog/test_changelog.py +++ b/tests/e2e/cmd_changelog/test_changelog.py @@ -77,6 +77,12 @@ from requests_mock import Mocker + from semantic_release.commit_parser.conventional.parser import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from tests.conftest import RunCliFn from tests.e2e.conftest import RetrieveRuntimeContextFn from tests.fixtures.example_project import ( @@ -867,9 +873,12 @@ def test_changelog_update_mode_unreleased_n_released( commit_n_rtn_changelog_entry: CommitNReturnChangelogEntryFn, changelog_file: Path, insertion_flag: str, - get_commit_def_of_conventional_commit: GetCommitDefFn, - get_commit_def_of_emoji_commit: GetCommitDefFn, - get_commit_def_of_scipy_commit: GetCommitDefFn, + get_commit_def_of_conventional_commit: GetCommitDefFn[ConventionalCommitParser], + get_commit_def_of_emoji_commit: GetCommitDefFn[EmojiCommitParser], + get_commit_def_of_scipy_commit: GetCommitDefFn[ScipyCommitParser], + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, ): """ Given there are unreleased changes and a previous release in the changelog, @@ -890,18 +899,23 @@ def test_changelog_update_mode_unreleased_n_released( commit_n_section: Commit2Section = { "conventional": { "commit": get_commit_def_of_conventional_commit( - "perf: improve the performance of the application" + "perf: improve the performance of the application", + parser=default_conventional_parser, ), "section": "Performance Improvements", }, "emoji": { "commit": get_commit_def_of_emoji_commit( - ":zap: improve the performance of the application" + ":zap: improve the performance of the application", + parser=default_emoji_parser, ), "section": ":zap:", }, "scipy": { - "commit": get_commit_def_of_scipy_commit("MAINT: fix an issue"), + "commit": get_commit_def_of_scipy_commit( + "MAINT: fix an issue", + parser=default_scipy_parser, + ), "section": "Fix", }, } diff --git a/tests/e2e/cmd_version/bump_version/conftest.py b/tests/e2e/cmd_version/bump_version/conftest.py index da36ff1d2..d319be2a1 100644 --- a/tests/e2e/cmd_version/bump_version/conftest.py +++ b/tests/e2e/cmd_version/bump_version/conftest.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from pathlib import Path - from typing import Protocol + from typing import Protocol, Sequence from click.testing import Result @@ -24,7 +24,8 @@ class InitMirrorRepo4RebuildFn(Protocol): def __call__( self, mirror_repo_dir: Path, - configuration_step: RepoActionConfigure, + configuration_steps: Sequence[RepoActionConfigure], + files_to_remove: Sequence[Path], ) -> Path: ... class RunPSReleaseFn(Protocol): @@ -32,18 +33,18 @@ def __call__( self, next_version_str: str, git_repo: Repo, + config_toml_path: Path = ..., ) -> Result: ... @pytest.fixture(scope="session") def init_mirror_repo_for_rebuild( build_repo_from_definition: BuildRepoFromDefinitionFn, - changelog_md_file: Path, - changelog_rst_file: Path, ) -> InitMirrorRepo4RebuildFn: def _init_mirror_repo_for_rebuild( mirror_repo_dir: Path, - configuration_step: RepoActionConfigure, + configuration_steps: Sequence[RepoActionConfigure], + files_to_remove: Sequence[Path], ) -> Path: # Create the mirror repo directory mirror_repo_dir.mkdir(exist_ok=True, parents=True) @@ -51,13 +52,23 @@ def _init_mirror_repo_for_rebuild( # Initialize mirror repository build_repo_from_definition( dest_dir=mirror_repo_dir, - repo_construction_steps=[configuration_step], + repo_construction_steps=configuration_steps, ) with Repo(mirror_repo_dir) as mirror_git_repo: - # remove the default changelog files to enable Update Mode (new default of v10) - mirror_git_repo.git.rm(str(changelog_md_file), force=True) - mirror_git_repo.git.rm(str(changelog_rst_file), force=True) + for filepath in files_to_remove: + file = ( + (mirror_git_repo.working_dir / filepath).resolve().absolute() + if not filepath.is_absolute() + else filepath + ) + if ( + not file.is_relative_to(mirror_git_repo.working_dir) + or not file.exists() + ): + continue + + mirror_git_repo.git.rm(str(file), force=True) return mirror_repo_dir @@ -69,6 +80,7 @@ def run_psr_release( run_cli: RunCliFn, changelog_rst_file: Path, update_pyproject_toml: UpdatePyprojectTomlFn, + pyproject_toml_file: Path, ) -> RunPSReleaseFn: base_version_cmd = [MAIN_PROG_NAME, "--strict", VERSION_SUBCMD] write_changelog_only_cmd = [ @@ -82,6 +94,7 @@ def run_psr_release( def _run_psr_release( next_version_str: str, git_repo: Repo, + config_toml_path: Path = pyproject_toml_file, ) -> Result: version_n_buildmeta = next_version_str.split("+", maxsplit=1) version_n_prerelease = version_n_buildmeta[0].split("-", maxsplit=1) @@ -107,6 +120,7 @@ def _run_psr_release( update_pyproject_toml( "tool.semantic_release.changelog.default_templates.changelog_file", str(changelog_rst_file), + toml_file=config_toml_path, ) cli_cmd = [*write_changelog_only_cmd, *prerelease_args, *build_metadata_args] result = run_cli(cli_cmd[1:], env={Github.DEFAULT_ENV_TOKEN_NAME: "1234"}) @@ -116,7 +130,7 @@ def _run_psr_release( git_repo.git.reset("--mixed", "HEAD") # Add the changelog file to the git index but reset the working directory - git_repo.git.add(str(changelog_rst_file)) + git_repo.git.add(str(changelog_rst_file.resolve())) git_repo.git.checkout("--", ".") # Actual run to release & write the MD changelog diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 209f3654e..2cfcd67ea 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -34,6 +34,7 @@ class GetSanitizedChangelogContentFn(Protocol): def __call__( self, repo_dir: Path, + changelog_file: Path = ..., remove_insertion_flag: bool = True, ) -> str: ... @@ -81,7 +82,7 @@ def config_path(example_project_dir: ExProjectDir) -> Path: return example_project_dir / DEFAULT_CONFIG_FILE -@pytest.fixture +@pytest.fixture(scope="session") def read_config_file() -> ReadConfigFileFn: def _read_config_file(file: Path | str) -> RawConfig: config_text = load_raw_config_file(file) @@ -136,12 +137,12 @@ def _strip_logging_messages(log: str) -> str: @pytest.fixture(scope="session") -def long_hash_pattern() -> Pattern: +def long_hash_pattern() -> Pattern[str]: return regexp(r"\b([0-9a-f]{40})\b", IGNORECASE) @pytest.fixture(scope="session") -def short_hash_pattern() -> Pattern: +def short_hash_pattern() -> Pattern[str]: return regexp(r"\b([0-9a-f]{7})\b", IGNORECASE) @@ -149,18 +150,22 @@ def short_hash_pattern() -> Pattern: def get_sanitized_rst_changelog_content( changelog_rst_file: Path, default_rst_changelog_insertion_flag: str, - long_hash_pattern: Pattern, - short_hash_pattern: Pattern, + long_hash_pattern: Pattern[str], + short_hash_pattern: Pattern[str], ) -> GetSanitizedChangelogContentFn: rst_short_hash_link_pattern = regexp(r"(_[0-9a-f]{7})\b", IGNORECASE) def _get_sanitized_rst_changelog_content( repo_dir: Path, + changelog_file: Path = changelog_rst_file, remove_insertion_flag: bool = False, ) -> str: + if not (changelog_path := repo_dir / changelog_file).exists(): + return "" + # Note that our repo generation fixture includes the insertion flag automatically # toggle remove_insertion_flag to True to remove the insertion flag, applies to Init mode repos - with (repo_dir / changelog_rst_file).open(newline=os.linesep) as rfd: + with changelog_path.open(newline=os.linesep) as rfd: # use os.linesep here because the insertion flag is os-specific # but convert the content to universal newlines for comparison changelog_content = ( @@ -182,16 +187,20 @@ def _get_sanitized_rst_changelog_content( def get_sanitized_md_changelog_content( changelog_md_file: Path, default_md_changelog_insertion_flag: str, - long_hash_pattern: Pattern, - short_hash_pattern: Pattern, + long_hash_pattern: Pattern[str], + short_hash_pattern: Pattern[str], ) -> GetSanitizedChangelogContentFn: def _get_sanitized_md_changelog_content( repo_dir: Path, + changelog_file: Path = changelog_md_file, remove_insertion_flag: bool = False, ) -> str: + if not (changelog_path := repo_dir / changelog_file).exists(): + return "" + # Note that our repo generation fixture includes the insertion flag automatically # toggle remove_insertion_flag to True to remove the insertion flag, applies to Init mode repos - with (repo_dir / changelog_md_file).open(newline=os.linesep) as rfd: + with changelog_path.open(newline=os.linesep) as rfd: # use os.linesep here because the insertion flag is os-specific # but convert the content to universal newlines for comparison changelog_content = ( diff --git a/tests/e2e/test_main.py b/tests/e2e/test_main.py index 8ce3c58a5..45f13d28b 100644 --- a/tests/e2e/test_main.py +++ b/tests/e2e/test_main.py @@ -3,6 +3,7 @@ import json import subprocess from pathlib import Path +from shutil import rmtree from textwrap import dedent from typing import TYPE_CHECKING @@ -18,8 +19,6 @@ from tests.util import assert_exit_code, assert_successful_exit_code if TYPE_CHECKING: - from pathlib import Path - from tests.conftest import RunCliFn from tests.e2e.conftest import StripLoggingMessagesFn from tests.fixtures.example_project import ExProjectDir, UpdatePyprojectTomlFn @@ -245,7 +244,10 @@ def test_uses_default_config_when_no_config_file_found( # We have to initialise an empty git repository, as the example projects # all have pyproject.toml configs which would be used by default with git.Repo.init(example_project_dir) as repo: + rmtree(str(Path(repo.git_dir, "hooks"))) + repo.git.branch("-M", "main") + with repo.config_writer("repository") as config: config.set_value("user", "name", "semantic release testing") config.set_value("user", "email", "not_a_real@email.com") diff --git a/tests/fixtures/example_project.py b/tests/fixtures/example_project.py index e034cb0a4..8cad54dc5 100644 --- a/tests/fixtures/example_project.py +++ b/tests/fixtures/example_project.py @@ -4,6 +4,7 @@ from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING, Generator, cast +from unittest import mock import pytest import tomlkit @@ -12,6 +13,12 @@ from importlib_resources import files import semantic_release +from semantic_release.cli.config import ( + GlobalCommandLineOptions, + RawConfig, + RuntimeContext, +) +from semantic_release.cli.util import load_raw_config_file from semantic_release.commit_parser import ( ConventionalCommitParser, EmojiCommitParser, @@ -54,7 +61,7 @@ ExProjectDir = Path class GetWheelFileFn(Protocol): - def __call__(self, version_str: str) -> Path: ... + def __call__(self, version_str: str, pkg_name: str = ...) -> Path: ... class SetFlagFn(Protocol): def __call__(self, flag: bool, toml_file: Path | str = ...) -> None: ... @@ -87,6 +94,33 @@ def __call__( self, version: Version | str, version_file: Path | str = ... ) -> None: ... + class GetHvcsFn(Protocol): + def __call__( + self, + hvcs_client_name: str, + origin_url: str = ..., + hvcs_domain: str | None = None, + ) -> Github | Gitlab | Gitea | Bitbucket: ... + + class ReadConfigFileFn(Protocol): + """Read the raw config file from `config_path`.""" + + def __call__(self, file: Path | str = ...) -> RawConfig: ... + + class LoadRuntimeContextFn(Protocol): + """Load the runtime context from the config file.""" + + def __call__( + self, cli_opts: GlobalCommandLineOptions | None = None + ) -> RuntimeContext: ... + + class GetParserFromConfigFileFn(Protocol): + """Get the commit parser from the config file.""" + + def __call__( + self, file: Path | str = ... + ) -> CommitParser[ParseResult, ParserOptions]: ... + @pytest.fixture(scope="session") def deps_files_4_example_project() -> list[Path]: @@ -282,12 +316,58 @@ def default_changelog_rst_template() -> Path: @pytest.fixture(scope="session") def get_wheel_file(dist_dir: Path) -> GetWheelFileFn: - def _get_wheel_file(version_str: str) -> Path: - return dist_dir / f"{EXAMPLE_PROJECT_NAME}-{version_str}-py3-none-any.whl" + def _get_wheel_file( + version_str: str, + pkg_name: str = EXAMPLE_PROJECT_NAME, + ) -> Path: + return dist_dir.joinpath( + f"{pkg_name.replace('-', '_')}-{version_str}-py3-none-any.whl" + ) return _get_wheel_file +@pytest.fixture(scope="session") +def read_config_file(pyproject_toml_file: Path) -> ReadConfigFileFn: + def _read_config_file(file: Path | str = pyproject_toml_file) -> RawConfig: + config_text = load_raw_config_file(file) + return RawConfig.model_validate(config_text) + + return _read_config_file + + +@pytest.fixture(scope="session") +def load_runtime_context( + read_config_file: ReadConfigFileFn, + pyproject_toml_file: Path, +) -> LoadRuntimeContextFn: + def _load_runtime_context( + cli_opts: GlobalCommandLineOptions | None = None, + ) -> RuntimeContext: + opts = cli_opts or GlobalCommandLineOptions( + config_file=str(pyproject_toml_file), + ) + raw_config = read_config_file(opts.config_file) + return RuntimeContext.from_raw_config(raw_config, opts) + + return _load_runtime_context + + +@pytest.fixture(scope="session") +def get_parser_from_config_file( + pyproject_toml_file: Path, + load_runtime_context: LoadRuntimeContextFn, +) -> GetParserFromConfigFileFn: + def _get_parser_from_config( + file: Path | str = pyproject_toml_file, + ) -> CommitParser[ParseResult, ParserOptions]: + return load_runtime_context( + cli_opts=GlobalCommandLineOptions(config_file=str(Path(file))) + ).commit_parser + + return _get_parser_from_config + + @pytest.fixture def example_project_dir(tmp_path: Path) -> ExProjectDir: return tmp_path.resolve() @@ -572,6 +652,34 @@ def _use_custom_parser( return _use_custom_parser +@pytest.fixture(scope="session") +def get_hvcs(example_git_https_url: str) -> GetHvcsFn: + hvcs_clients: dict[str, type[HvcsBase]] = { + "github": Github, + "gitlab": Gitlab, + "gitea": Gitea, + "bitbucket": Bitbucket, + } + + def _get_hvcs( + hvcs_client_name: str, + origin_url: str = example_git_https_url, + hvcs_domain: str | None = None, + ) -> Github | Gitlab | Gitea | Bitbucket: + if (hvcs_class := hvcs_clients.get(hvcs_client_name)) is None: + raise ValueError(f"Unknown HVCS client name: {hvcs_client_name}") + + # Create HVCS Client instance + with mock.patch.dict(os.environ, {}, clear=True): + hvcs = hvcs_class(origin_url, hvcs_domain=hvcs_domain) + assert hvcs.repo_name # Force the HVCS client to cache the repo name + assert hvcs.owner # Force the HVCS client to cache the owner + + return cast("Github | Gitlab | Gitea | Bitbucket", hvcs) + + return _get_hvcs + + @pytest.fixture(scope="session") def use_github_hvcs( pyproject_toml_file: Path, diff --git a/tests/fixtures/git_repo.py b/tests/fixtures/git_repo.py index 7ad6ac0be..f76f83455 100644 --- a/tests/fixtures/git_repo.py +++ b/tests/fixtures/git_repo.py @@ -5,22 +5,18 @@ from copy import deepcopy from datetime import datetime, timedelta from functools import reduce +from itertools import count from pathlib import Path +from shutil import rmtree from textwrap import dedent from time import sleep -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, TypeVar, cast from unittest import mock import pytest from git import Actor, Repo from semantic_release.cli.config import ChangelogOutputFormat -from semantic_release.commit_parser.conventional import ( - ConventionalCommitParser, - ConventionalCommitParserOptions, -) -from semantic_release.commit_parser.emoji import EmojiCommitParser, EmojiParserOptions -from semantic_release.commit_parser.scipy import ScipyCommitParser, ScipyParserOptions from semantic_release.hvcs.bitbucket import Bitbucket from semantic_release.hvcs.gitea import Gitea from semantic_release.hvcs.github import Github @@ -35,6 +31,7 @@ DEFAULT_BRANCH_NAME, DEFAULT_MERGE_STRATEGY_OPTION, EXAMPLE_HVCS_DOMAIN, + EXAMPLE_PROJECT_NAME, EXAMPLE_REPO_NAME, EXAMPLE_REPO_OWNER, NULL_HEX_SHA, @@ -47,9 +44,35 @@ ) if TYPE_CHECKING: - from typing import Any, Generator, Literal, Protocol, Sequence, TypedDict, Union + from typing import ( + Any, + Dict, + Generator, + Generic, + List, + Literal, + Protocol, + Sequence, + Set, + Tuple, + TypedDict, + TypeVar, + Union, + ) - from tests.fixtures.example_project import UpdateVersionPyFileFn + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParsedMessageResult, ParseResult + + from tests.fixtures.example_project import ( + GetHvcsFn, + GetParserFromConfigFileFn, + UpdateVersionPyFileFn, + ) try: # Python 3.8 and 3.9 compatibility @@ -80,7 +103,9 @@ CommitMsg = str DatetimeISOStr = str ChangelogTypeHeading = str - TomlSerializableTypes = Union[dict, set, list, tuple, int, float, bool, str] + TomlSerializableTypes = Union[ + Dict[Any, Any], Set[Any], List[Any], Tuple[Any, ...], int, float, bool, str + ] class RepoVersionDef(TypedDict): """ @@ -101,6 +126,7 @@ class ChangelogTypeHeadingDef(TypedDict): """List of indexes values to match to the commits list in the RepoVersionDef""" class CommitDef(TypedDict): + cid: str msg: CommitMsg type: str category: str @@ -128,6 +154,7 @@ def __call__( tag_format_str: str | None = None, extra_configs: dict[str, TomlSerializableTypes] | None = None, mask_initial_release: bool = True, # Default as of v10 + package_name: str = ..., ) -> tuple[Path, HvcsBase]: ... class CommitNReturnChangelogEntryFn(Protocol): @@ -145,6 +172,8 @@ def __call__( version: str, tag_format: str = ..., timestamp: DatetimeISOStr | None = None, + version_py_file: Path | str = ..., + commit_message_format: str = ..., ) -> None: ... class ExProjectGitRepoFn(Protocol): @@ -157,8 +186,10 @@ def __call__( commit_type: CommitConvention, ) -> RepoDefinition: ... - class GetCommitDefFn(Protocol): - def __call__(self, msg: str) -> CommitDef: ... + T_contra = TypeVar("T_contra", contravariant=True) + + class GetCommitDefFn(Protocol[T_contra]): + def __call__(self, msg: str, parser: T_contra) -> CommitDef: ... class GetVersionStringsFn(Protocol): def __call__(self) -> list[VersionStr]: ... @@ -183,7 +214,7 @@ def __call__( repo_definition: RepoDefinition, hvcs: Github | Gitlab | Gitea | Bitbucket, dest_file: Path | None = None, - max_version: str | None = None, + max_version: Version | Literal["Unreleased"] | None = None, output_format: ChangelogOutputFormat = ChangelogOutputFormat.MARKDOWN, mask_initial_release: bool = True, # Default as of v10 ) -> str: ... @@ -247,6 +278,7 @@ def __call__( ) -> CommitDef: ... class CommitSpec(TypedDict): + cid: str conventional: str emoji: str scipy: str @@ -281,8 +313,11 @@ class RepoActionRelease(TypedDict): details: RepoActionReleaseDetails class RepoActionReleaseDetails(DetailsBase): - version: str + commit_message_format: NotRequired[str] datetime: DatetimeISOStr + tag_format: NotRequired[str] + version: str + version_py_file: NotRequired[Path | str] class RepoActionGitCheckout(TypedDict): action: Literal[RepoActionStep.GIT_CHECKOUT] @@ -304,10 +339,7 @@ class RepoActionGitSquashDetails(DetailsBase): branch: str strategy_option: str commit_def: CommitDef - - class RepoActionGitMerge(TypedDict): - action: Literal[RepoActionStep.GIT_MERGE] - details: RepoActionGitMergeDetails | RepoActionGitFFMergeDetails + config_file: NotRequired[Path | str] class RepoActionGitMergeDetails(DetailsBase): branch_name: str @@ -319,22 +351,36 @@ class RepoActionGitFFMergeDetails(DetailsBase): branch_name: str fast_forward: Literal[True] + MergeDetails = TypeVar( + "MergeDetails", + bound=Union[RepoActionGitMergeDetails, RepoActionGitFFMergeDetails], + ) + + class RepoActionGitMerge(Generic[MergeDetails], TypedDict): + action: Literal[RepoActionStep.GIT_MERGE] + details: MergeDetails + class RepoActionWriteChangelogs(TypedDict): action: Literal[RepoActionStep.WRITE_CHANGELOGS] details: RepoActionWriteChangelogsDetails class RepoActionWriteChangelogsDetails(DetailsBase): - new_version: str - max_version: NotRequired[str] + new_version: Version | Literal["Unreleased"] + max_version: NotRequired[Version | Literal["Unreleased"]] dest_files: Sequence[RepoActionWriteChangelogsDestFile] + commit_ids: Sequence[str] class RepoActionWriteChangelogsDestFile(TypedDict): path: Path | str format: ChangelogOutputFormat + mask_initial_release: bool class ConvertCommitSpecToCommitDefFn(Protocol): def __call__( - self, commit_spec: CommitSpec, commit_type: CommitConvention + self, + commit_spec: CommitSpec, + commit_type: CommitConvention, + parser: CommitParser[ParseResult, ParserOptions], ) -> CommitDef: ... class GetRepoDefinitionFn(Protocol): @@ -361,11 +407,14 @@ class BuiltRepoResult(TypedDict): repo: Repo class GetVersionsFromRepoBuildDefFn(Protocol): - def __call__(self, repo_def: Sequence[RepoActions]) -> Sequence[str]: ... + def __call__(self, repo_def: Sequence[RepoActions]) -> Sequence[Version]: ... class ConvertCommitSpecsToCommitDefsFn(Protocol): def __call__( - self, commits: Sequence[CommitSpec], commit_type: CommitConvention + self, + commits: Sequence[CommitSpec], + commit_type: CommitConvention, + parser: CommitParser[ParseResult, ParserOptions], ) -> Sequence[CommitDef]: ... class BuildSpecificRepoFn(Protocol): @@ -375,12 +424,13 @@ def __call__( RepoActions: TypeAlias = Union[ RepoActionConfigure, - RepoActionMakeCommits, - RepoActionRelease, RepoActionGitCheckout, + RepoActionGitMerge[RepoActionGitMergeDetails], + RepoActionGitMerge[RepoActionGitFFMergeDetails], RepoActionGitSquash, + RepoActionMakeCommits, + RepoActionRelease, RepoActionWriteChangelogs, - RepoActionGitMerge, ] class GetGitRepo4DirFn(Protocol): @@ -388,16 +438,24 @@ def __call__(self, directory: Path | str) -> Repo: ... class SplitRepoActionsByReleaseTagsFn(Protocol): def __call__( - self, repo_definition: Sequence[RepoActions], tag_format_str: str - ) -> dict[str, list[RepoActions]]: ... + self, + repo_definition: Sequence[RepoActions], + ) -> dict[Version | Literal["Unreleased"] | None, list[RepoActions]]: ... class GetCfgValueFromDefFn(Protocol): def __call__( self, build_definition: Sequence[RepoActions], key: str ) -> Any: ... + class SquashedCommitSupportedParser(Protocol): + def unsquash_commit_message(self, message: str) -> list[str]: ... + + def parse_message(self, message: str) -> ParsedMessageResult | None: ... + class SeparateSquashedCommitDefFn(Protocol): - def __call__(self, squashed_commit_def: CommitDef) -> list[CommitDef]: ... + def __call__( + self, squashed_commit_def: CommitDef, parser: SquashedCommitSupportedParser + ) -> list[CommitDef]: ... class GenerateDefaultReleaseNotesFromDefFn(Protocol): def __call__( @@ -469,6 +527,7 @@ def _build_repo(cached_repo_path: Path) -> Sequence[RepoActions]: # NOTE: We don't want to hold the repo object open for the entire test session, # the implementation on Windows holds some file descriptors open until close is called. with Repo.init(cached_repo_path) as repo: + rmtree(str(Path(repo.git_dir, "hooks"))) # Without this the global config may set it to "master", we want consistency repo.git.branch("-M", DEFAULT_BRANCH_NAME) with repo.config_writer("repository") as config: @@ -520,12 +579,11 @@ def example_git_https_url(): @pytest.fixture(scope="session") -def get_commit_def_of_conventional_commit( - default_conventional_parser: ConventionalCommitParser, -) -> GetCommitDefFn: - def _get_commit_def_of_conventional_commit(msg: str) -> CommitDef: - if not (parsed_result := default_conventional_parser.parse_message(msg)): +def get_commit_def_of_conventional_commit() -> GetCommitDefFn[ConventionalCommitParser]: + def _get_commit_def(msg: str, parser: ConventionalCommitParser) -> CommitDef: + if not (parsed_result := parser.parse_message(msg)): return { + "cid": "", "msg": msg, "type": "unknown", "category": "Unknown", @@ -538,6 +596,7 @@ def _get_commit_def_of_conventional_commit(msg: str) -> CommitDef: } return { + "cid": "", "msg": msg, "type": parsed_result.type, "category": parsed_result.category, @@ -549,16 +608,17 @@ def _get_commit_def_of_conventional_commit(msg: str) -> CommitDef: "include_in_changelog": True, } - return _get_commit_def_of_conventional_commit + return _get_commit_def @pytest.fixture(scope="session") -def get_commit_def_of_emoji_commit( - default_emoji_parser: EmojiCommitParser, -) -> GetCommitDefFn: - def _get_commit_def_of_emoji_commit(msg: str) -> CommitDef: - if not (parsed_result := default_emoji_parser.parse_message(msg)): +def get_commit_def_of_emoji_commit() -> GetCommitDefFn[EmojiCommitParser]: + def _get_commit_def_of_emoji_commit( + msg: str, parser: EmojiCommitParser + ) -> CommitDef: + if not (parsed_result := parser.parse_message(msg)): return { + "cid": "", "msg": msg, "type": "unknown", "category": "Other", @@ -571,6 +631,7 @@ def _get_commit_def_of_emoji_commit(msg: str) -> CommitDef: } return { + "cid": "", "msg": msg, "type": parsed_result.type, "category": parsed_result.category, @@ -586,12 +647,13 @@ def _get_commit_def_of_emoji_commit(msg: str) -> CommitDef: @pytest.fixture(scope="session") -def get_commit_def_of_scipy_commit( - default_scipy_parser: ScipyCommitParser, -) -> GetCommitDefFn: - def _get_commit_def_of_scipy_commit(msg: str) -> CommitDef: - if not (parsed_result := default_scipy_parser.parse_message(msg)): +def get_commit_def_of_scipy_commit() -> GetCommitDefFn[ScipyCommitParser]: + def _get_commit_def_of_scipy_commit( + msg: str, parser: ScipyCommitParser + ) -> CommitDef: + if not (parsed_result := parser.parse_message(msg)): return { + "cid": "", "msg": msg, "type": "unknown", "category": "Unknown", @@ -604,6 +666,7 @@ def _get_commit_def_of_scipy_commit(msg: str) -> CommitDef: } return { + "cid": "", "msg": msg, "type": parsed_result.type, "category": parsed_result.category, @@ -715,17 +778,27 @@ def _format_squash_commit_msg_github( pr_number: int, squashed_commits: list[CommitDef | str], ) -> str: - sq_cmts: list[str] = ( - squashed_commits # type: ignore[assignment] + sq_commits: list[str] = ( + cast("list[str]", squashed_commits) if len(squashed_commits) > 1 and not isinstance(squashed_commits[0], dict) - else [commit["msg"] for commit in squashed_commits] # type: ignore[index] + else list( + filter( + None, + [ + commit.get("msg", "") if isinstance(commit, dict) else commit + for commit in squashed_commits + ], + ) + ) ) + pr_title_parts = pr_title.strip().split("\n\n", maxsplit=1) return ( str.join( "\n\n", [ - f"{pr_title} (#{pr_number})", - *[f"* {commit_str}" for commit_str in sq_cmts], + f"{pr_title_parts[0]} (#{pr_number})", + *pr_title_parts[1:], + *[f"* {commit_str}" for commit_str in sq_commits], ], ) + "\n" @@ -852,9 +925,11 @@ def create_release_tagged_commit( ) -> CreateReleaseFn: def _mimic_semantic_release_commit( git_repo: Repo, - version: str, + version: Version | str, tag_format: str = default_tag_format_str, timestamp: DatetimeISOStr | None = None, + version_py_file: Path | str = "", + commit_message_format: str = COMMIT_MESSAGE, ) -> None: curr_dt = stable_now_date() commit_dt = ( @@ -865,15 +940,18 @@ def _mimic_semantic_release_commit( sleep(1) # ensure commit timestamps are unique # stamp version into version file - update_version_py_file(version) + if version_py_file: + update_version_py_file(version=version, version_file=version_py_file) + else: + update_version_py_file(version=version) # stamp version into pyproject.toml - update_pyproject_toml("tool.poetry.version", version) + update_pyproject_toml("tool.poetry.version", str(version)) # commit --all files with version number commit message git_repo.git.commit( a=True, - m=COMMIT_MESSAGE.format(version=version), + m=commit_message_format.format(version=str(version)), date=commit_dt.isoformat(timespec="seconds"), ) @@ -884,7 +962,7 @@ def _mimic_semantic_release_commit( GIT_COMMITTER_DATE=commit_dt.isoformat(timespec="seconds"), ): # tag commit with version number - tag_str = tag_format.format(version=version) + tag_str = tag_format.format(version=str(version)) git_repo.git.tag(tag_str, m=tag_str) return _mimic_semantic_release_commit @@ -932,10 +1010,13 @@ def simulate_change_commits_n_rtn_changelog_entry( def _simulate_change_commits_n_rtn_changelog_entry( git_repo: Repo, commit_msgs: Sequence[CommitDef] ) -> Sequence[CommitDef]: - changelog_entries = [] + changelog_entries: list[CommitDef] = [] for commit_msg in commit_msgs: - add_text_to_file(git_repo, file_in_repo) + if not git_repo.is_dirty(index=True, working_tree=False): + add_text_to_file(git_repo, file_in_repo) + changelog_entries.append(commit_n_rtn_changelog_entry(git_repo, commit_msg)) + return changelog_entries return _simulate_change_commits_n_rtn_changelog_entry @@ -970,6 +1051,7 @@ def _get_hvcs_client_from_repo_def( ) # Force the HVCS client to attempt to resolve the repo name (as we generally cache it) assert hvcs_client.repo_name + assert hvcs_client.owner return cast("Github | Gitlab | Gitea | Bitbucket", hvcs_client) return _get_hvcs_client_from_repo_def @@ -978,6 +1060,47 @@ def _get_hvcs_client_from_repo_def( @pytest.fixture(scope="session") def build_configured_base_repo( # noqa: C901 cached_example_git_project: Path, + configure_base_repo: BuildRepoFn, +) -> BuildRepoFn: + """ + This fixture is intended to simplify repo scenario building by initially + creating the repo but also configuring semantic_release in the pyproject.toml + for when the test executes semantic_release. It returns a function so that + derivative fixtures can call this fixture with individual parameters. + """ + + def _build_configured_base_repo( # noqa: C901 + dest_dir: Path | str, + commit_type: CommitConvention = "conventional", + hvcs_client_name: str = "github", + hvcs_domain: str = EXAMPLE_HVCS_DOMAIN, + tag_format_str: str | None = None, + extra_configs: dict[str, TomlSerializableTypes] | None = None, + mask_initial_release: bool = True, # Default as of v10 + package_name: str = EXAMPLE_PROJECT_NAME, + ) -> tuple[Path, HvcsBase]: + if not cached_example_git_project.exists(): + raise RuntimeError("Unable to find cached git project files!") + + # Copy the cached git project the dest directory + copy_dir_tree(cached_example_git_project, dest_dir) + + return configure_base_repo( + dest_dir=dest_dir, + commit_type=commit_type, + hvcs_client_name=hvcs_client_name, + hvcs_domain=hvcs_domain, + tag_format_str=tag_format_str, + extra_configs=extra_configs, + mask_initial_release=mask_initial_release, + package_name=package_name, + ) + + return _build_configured_base_repo + + +@pytest.fixture(scope="session") +def configure_base_repo( # noqa: C901 use_github_hvcs: UseHvcsFn, use_gitlab_hvcs: UseHvcsFn, use_gitea_hvcs: UseHvcsFn, @@ -989,6 +1112,8 @@ def build_configured_base_repo( # noqa: C901 example_git_https_url: str, update_pyproject_toml: UpdatePyprojectTomlFn, get_wheel_file: GetWheelFileFn, + pyproject_toml_file: Path, + get_hvcs: GetHvcsFn, ) -> BuildRepoFn: """ This fixture is intended to simplify repo scenario building by initially @@ -997,7 +1122,7 @@ def build_configured_base_repo( # noqa: C901 derivative fixtures can call this fixture with individual parameters. """ - def _build_configured_base_repo( # noqa: C901 + def _configure_base_repo( # noqa: C901 dest_dir: Path | str, commit_type: str = "conventional", hvcs_client_name: str = "github", @@ -1005,53 +1130,52 @@ def _build_configured_base_repo( # noqa: C901 tag_format_str: str | None = None, extra_configs: dict[str, TomlSerializableTypes] | None = None, mask_initial_release: bool = True, # Default as of v10 + package_name: str = EXAMPLE_PROJECT_NAME, ) -> tuple[Path, HvcsBase]: - if not cached_example_git_project.exists(): - raise RuntimeError("Unable to find cached git project files!") - - # Copy the cached git project the dest directory - copy_dir_tree(cached_example_git_project, dest_dir) - # Make sure we are in the dest directory with temporary_working_directory(dest_dir): # Set parser configuration if commit_type == "conventional": - use_conventional_parser() + use_conventional_parser(toml_file=pyproject_toml_file) elif commit_type == "emoji": - use_emoji_parser() + use_emoji_parser(toml_file=pyproject_toml_file) elif commit_type == "scipy": - use_scipy_parser() + use_scipy_parser(toml_file=pyproject_toml_file) else: - use_custom_parser(commit_type) + use_custom_parser(commit_type, toml_file=pyproject_toml_file) # Set HVCS configuration if hvcs_client_name == "github": - hvcs_class = use_github_hvcs(hvcs_domain) + use_github_hvcs(hvcs_domain, toml_file=pyproject_toml_file) elif hvcs_client_name == "gitlab": - hvcs_class = use_gitlab_hvcs(hvcs_domain) + use_gitlab_hvcs(hvcs_domain, toml_file=pyproject_toml_file) elif hvcs_client_name == "gitea": - hvcs_class = use_gitea_hvcs(hvcs_domain) + use_gitea_hvcs(hvcs_domain, toml_file=pyproject_toml_file) elif hvcs_client_name == "bitbucket": - hvcs_class = use_bitbucket_hvcs(hvcs_domain) + use_bitbucket_hvcs(hvcs_domain, toml_file=pyproject_toml_file) else: raise ValueError(f"Unknown HVCS client name: {hvcs_client_name}") # Create HVCS Client instance - with mock.patch.dict(os.environ, {}, clear=True): - hvcs = hvcs_class(example_git_https_url, hvcs_domain=hvcs_domain) - assert hvcs.repo_name # Force the HVCS client to cache the repo name + hvcs = get_hvcs( + hvcs_client_name=hvcs_client_name, + origin_url=example_git_https_url, + hvcs_domain=hvcs_domain, + ) # Set tag format in configuration if tag_format_str is not None: update_pyproject_toml( - "tool.semantic_release.tag_format", tag_format_str + "tool.semantic_release.tag_format", + tag_format_str, + toml_file=pyproject_toml_file, ) # Set the build_command to create a wheel file (using the build_command_env version variable) build_result_file = ( - get_wheel_file("$NEW_VERSION") + get_wheel_file("$NEW_VERSION", pkg_name=package_name) if sys.platform != "win32" - else get_wheel_file("$Env:NEW_VERSION") + else get_wheel_file("$Env:NEW_VERSION", pkg_name=package_name) ) update_pyproject_toml( # NOTE: must work in both bash and Powershell @@ -1071,69 +1195,65 @@ def _build_configured_base_repo( # noqa: C901 New-Item -ItemType file -Path "$WHEEL_FILE" -Force | Select-Object OriginalPath """ ), + toml_file=pyproject_toml_file, ) # Set whether or not the initial release should be masked update_pyproject_toml( "tool.semantic_release.changelog.default_templates.mask_initial_release", mask_initial_release, + toml_file=pyproject_toml_file, ) # Apply configurations to pyproject.toml if extra_configs is not None: for key, value in extra_configs.items(): - update_pyproject_toml(key, value) + update_pyproject_toml(key, value, toml_file=pyproject_toml_file) return Path(dest_dir), hvcs - return _build_configured_base_repo + return _configure_base_repo @pytest.fixture(scope="session") -def separate_squashed_commit_def( - default_conventional_parser: ConventionalCommitParser, - default_emoji_parser: EmojiCommitParser, - default_scipy_parser: ScipyCommitParser, -) -> SeparateSquashedCommitDefFn: - message_parsers: dict[ - CommitConvention, - ConventionalCommitParser | EmojiCommitParser | ScipyCommitParser, - ] = { - "conventional": ConventionalCommitParser( - options=ConventionalCommitParserOptions( - **{ - **default_conventional_parser.options.__dict__, - "parse_squash_commits": True, - } - ) - ), - "emoji": EmojiCommitParser( - options=EmojiParserOptions( - **{ - **default_emoji_parser.options.__dict__, - "parse_squash_commits": True, - } - ) - ), - "scipy": ScipyCommitParser( - options=ScipyParserOptions( - **{ - **default_scipy_parser.options.__dict__, - "parse_squash_commits": True, - } - ) - ), - } +def separate_squashed_commit_def() -> SeparateSquashedCommitDefFn: + # default_conventional_parser: ConventionalCommitParser, + # default_emoji_parser: EmojiCommitParser, + # default_scipy_parser: ScipyCommitParser, + # message_parsers: dict[ + # CommitConvention, + # ConventionalCommitParser | EmojiCommitParser | ScipyCommitParser, + # ] = { + # "conventional": ConventionalCommitParser( + # options=ConventionalCommitParserOptions( + # **{ + # **default_conventional_parser.options.__dict__, + # "parse_squash_commits": True, + # } + # ) + # ), + # "emoji": EmojiCommitParser( + # options=EmojiParserOptions( + # **{ + # **default_emoji_parser.options.__dict__, + # "parse_squash_commits": True, + # } + # ) + # ), + # "scipy": ScipyCommitParser( + # options=ScipyParserOptions( + # **{ + # **default_scipy_parser.options.__dict__, + # "parse_squash_commits": True, + # } + # ) + # ), + # } def _separate_squashed_commit_def( squashed_commit_def: CommitDef, + parser: SquashedCommitSupportedParser, ) -> list[CommitDef]: - commit_type: CommitConvention = "conventional" - for parser_name, parser in message_parsers.items(): - if squashed_commit_def["type"] in parser.options.allowed_tags: - commit_type = parser_name - - parser = message_parsers[commit_type] if not hasattr(parser, "unsquash_commit_message"): return [squashed_commit_def] @@ -1141,8 +1261,11 @@ def _separate_squashed_commit_def( message=squashed_commit_def["msg"] ) + commit_num_gen = (i for i in count(start=1, step=1)) + return [ { + "cid": f"{squashed_commit_def['cid']}-{next(commit_num_gen)}", "msg": squashed_message, "type": parsed_result.type, "category": parsed_result.category, @@ -1166,12 +1289,12 @@ def _separate_squashed_commit_def( @pytest.fixture(scope="session") def convert_commit_spec_to_commit_def( - get_commit_def_of_conventional_commit: GetCommitDefFn, - get_commit_def_of_emoji_commit: GetCommitDefFn, - get_commit_def_of_scipy_commit: GetCommitDefFn, + get_commit_def_of_conventional_commit: GetCommitDefFn[ConventionalCommitParser], + get_commit_def_of_emoji_commit: GetCommitDefFn[EmojiCommitParser], + get_commit_def_of_scipy_commit: GetCommitDefFn[ScipyCommitParser], stable_now_date: datetime, ) -> ConvertCommitSpecToCommitDefFn: - message_parsers: dict[CommitConvention, GetCommitDefFn] = { + message_parsers = { "conventional": get_commit_def_of_conventional_commit, "emoji": get_commit_def_of_emoji_commit, "scipy": get_commit_def_of_scipy_commit, @@ -1180,12 +1303,14 @@ def convert_commit_spec_to_commit_def( def _convert( commit_spec: CommitSpec, commit_type: CommitConvention, + parser: CommitParser[ParseResult, ParserOptions], ) -> CommitDef: - parse_msg_fn = message_parsers[commit_type] + parse_msg_fn = cast("GetCommitDefFn[Any]", message_parsers[commit_type]) # Extract the correct commit message for the commit type return { - **parse_msg_fn(commit_spec[commit_type]), + **parse_msg_fn(commit_spec[commit_type], parser=parser), + "cid": commit_spec["cid"], "datetime": ( commit_spec["datetime"] if "datetime" in commit_spec @@ -1204,9 +1329,11 @@ def convert_commit_specs_to_commit_defs( def _convert( commits: Sequence[CommitSpec], commit_type: CommitConvention, + parser: CommitParser[ParseResult, ParserOptions], ) -> Sequence[CommitDef]: return [ - convert_commit_spec_to_commit_def(commit, commit_type) for commit in commits + convert_commit_spec_to_commit_def(commit, commit_type, parser=parser) + for commit in commits ] return _convert @@ -1222,50 +1349,46 @@ def build_repo_from_definition( # noqa: C901, its required and its just test co simulate_change_commits_n_rtn_changelog_entry: SimulateChangeCommitsNReturnChangelogEntryFn, simulate_default_changelog_creation: SimulateDefaultChangelogCreationFn, separate_squashed_commit_def: SeparateSquashedCommitDefFn, + get_hvcs: GetHvcsFn, + example_git_https_url: str, + get_parser_from_config_file: GetParserFromConfigFileFn, + pyproject_toml_file: Path, ) -> BuildRepoFromDefinitionFn: def expand_repo_construction_steps( acc: Sequence[RepoActions], step: RepoActions ) -> Sequence[RepoActions]: - return [ - *acc, - *( - reduce( - expand_repo_construction_steps, # type: ignore[arg-type] - step["details"]["pre_actions"], - [], - ) - if "pre_actions" in step["details"] - else [] - ), - step, - *( - reduce( - expand_repo_construction_steps, # type: ignore[arg-type] - step["details"]["post_actions"], - [], - ) - if "post_actions" in step["details"] - else [] - ), - ] + empty_tuple = cast("tuple[RepoActions, ...]", ()) + unpacked_pre_actions = reduce( + expand_repo_construction_steps, # type: ignore[arg-type] + step["details"].pop("pre_actions", empty_tuple), + empty_tuple, + ) + + unpacked_post_actions = reduce( + expand_repo_construction_steps, # type: ignore[arg-type] + step["details"].pop("post_actions", empty_tuple), + empty_tuple, + ) + + return (*acc, *unpacked_pre_actions, step, *unpacked_post_actions) def _build_repo_from_definition( # noqa: C901, its required and its just test code dest_dir: Path | str, repo_construction_steps: Sequence[RepoActions] ) -> Sequence[RepoActions]: completed_repo_steps: list[RepoActions] = [] - expanded_repo_construction_steps: Sequence[RepoActions] = reduce( - expand_repo_construction_steps, - repo_construction_steps, - [], + expanded_repo_construction_steps: tuple[RepoActions, ...] = tuple( + reduce( + expand_repo_construction_steps, # type: ignore[arg-type] + repo_construction_steps, + (), + ) ) - repo_dir = Path(dest_dir) + repo_dir = Path(dest_dir).resolve().absolute() hvcs: Github | Gitlab | Gitea | Bitbucket - tag_format_str: str - mask_initial_release: bool = True # Default as of v10 - current_commits: list[CommitDef] = [] - current_repo_def: RepoDefinition = {} + commit_cache: dict[str, CommitDef] = {} + current_repo_def: dict[Version | Literal["Unreleased"], RepoVersionDef] = {} with temporary_working_directory(repo_dir): for step in expanded_repo_construction_steps: @@ -1273,11 +1396,12 @@ def _build_repo_from_definition( # noqa: C901, its required and its just test c action = step["action"] if action == RepoActionStep.CONFIGURE: - cfg_def: RepoActionConfigureDetails = step_result["details"] # type: ignore[assignment] + cfg_def = cast("RepoActionConfigureDetails", step_result["details"]) # Make sure the resulting build definition is complete with the default - tag_format_str = cfg_def["tag_format_str"] or default_tag_format_str - cfg_def["tag_format_str"] = tag_format_str + cfg_def["tag_format_str"] = ( + cfg_def["tag_format_str"] or default_tag_format_str + ) _, hvcs = build_configured_base_repo( # type: ignore[assignment] # TODO: fix the type error dest_dir, @@ -1293,14 +1417,11 @@ def _build_repo_from_definition( # noqa: C901, its required and its just test c ] }, ) - # Save configuration details for later steps - mask_initial_release = cfg_def["mask_initial_release"] - - # Make sure the resulting build definition is complete with the default - cfg_def["tag_format_str"] = tag_format_str elif action == RepoActionStep.MAKE_COMMITS: - mk_cmts_def: RepoActionMakeCommitsDetails = step_result["details"] # type: ignore[assignment] + mk_cmts_def = cast( + "RepoActionMakeCommitsDetails", step_result["details"] + ) # update the commit definitions with the repo hashes with Repo(repo_dir) as git_repo: @@ -1310,53 +1431,96 @@ def _build_repo_from_definition( # noqa: C901, its required and its just test c mk_cmts_def["commits"], ) ) - current_commits.extend( - filter( - lambda commit: commit["include_in_changelog"], - mk_cmts_def["commits"], + + for commit in mk_cmts_def["commits"]: + if commit["cid"] in commit_cache: + raise ValueError( + f"Duplicate commit id '{commit['cid']}' detected!" ) - ) + + commit_cache.update({commit["cid"]: commit}) elif action == RepoActionStep.WRITE_CHANGELOGS: - w_chlgs_def: RepoActionWriteChangelogsDetails = step["details"] # type: ignore[assignment] + w_chlgs_def = cast( + "RepoActionWriteChangelogsDetails", step["details"] + ) # Mark the repo definition with the latest stored commits for the upcoming release new_version = w_chlgs_def["new_version"] current_repo_def.update( - {new_version: {"commits": [*current_commits]}} + { + new_version: { + "commits": [ + *filter( + None, + ( + cmt + for commit_id in w_chlgs_def["commit_ids"] + if (cmt := commit_cache[commit_id])[ + "include_in_changelog" + ] + ), + ) + ] + } + } ) - current_commits.clear() - # Write each changelog with the current repo definition - for changelog_file_def in w_chlgs_def["dest_files"]: - simulate_default_changelog_creation( - current_repo_def, - hvcs=hvcs, - dest_file=repo_dir.joinpath(changelog_file_def["path"]), - output_format=changelog_file_def["format"], - mask_initial_release=mask_initial_release, - max_version=w_chlgs_def.get("max_version", None), + # in order to support monorepo changelogs we must filter and map the stored repo definition + # to match only the sub-package's versions which are identified by matching tag formats + filtered_repo_def_4_changelog: RepoDefinition = { + str(version): repo_def + for version, repo_def in current_repo_def.items() + if ( + isinstance(version, Version) + and isinstance(new_version, Version) + and version.tag_format == new_version.tag_format ) + or version == new_version + } + + # Write each changelog with the current repo definition + with Repo(repo_dir) as git_repo: + for changelog_file_def in w_chlgs_def["dest_files"]: + changelog_file = repo_dir.joinpath( + changelog_file_def["path"] + ) + simulate_default_changelog_creation( + filtered_repo_def_4_changelog, + hvcs=hvcs, + dest_file=changelog_file, + output_format=changelog_file_def["format"], + mask_initial_release=changelog_file_def[ + "mask_initial_release" + ], + max_version=w_chlgs_def.get("max_version"), + ) + + git_repo.git.add(str(changelog_file), force=True) elif action == RepoActionStep.RELEASE: - release_def: RepoActionReleaseDetails = step["details"] # type: ignore[assignment] + release_def = cast("RepoActionReleaseDetails", step["details"]) with Repo(repo_dir) as git_repo: create_release_tagged_commit( git_repo, version=release_def["version"], - tag_format=tag_format_str, + tag_format=release_def.get( + "tag_format", default_tag_format_str + ), timestamp=release_def["datetime"], + version_py_file=release_def.get("version_py_file", ""), + commit_message_format=release_def.get( + "commit_message_format", COMMIT_MESSAGE + ), ) elif action == RepoActionStep.GIT_CHECKOUT: - ckout_def: RepoActionGitCheckoutDetails = step["details"] # type: ignore[assignment] + ckout_def = cast("RepoActionGitCheckoutDetails", step["details"]) with Repo(repo_dir) as git_repo: if "create_branch" in ckout_def: - create_branch_def: RepoActionGitCheckoutCreateBranch = ( - ckout_def["create_branch"] - ) + create_branch_def = ckout_def["create_branch"] start_head = git_repo.heads[ create_branch_def["start_branch"] ] @@ -1370,7 +1534,9 @@ def _build_repo_from_definition( # noqa: C901, its required and its just test c git_repo.heads[ckout_def["branch"]].checkout() elif action == RepoActionStep.GIT_SQUASH: - squash_def: RepoActionGitSquashDetails = step_result["details"] # type: ignore[assignment] + squash_def = cast( + "RepoActionGitSquashDetails", step_result["details"] + ) # Update the commit definition with the repo hash with Repo(repo_dir) as git_repo: @@ -1380,27 +1546,44 @@ def _build_repo_from_definition( # noqa: C901, its required and its just test c commit_def=squash_def["commit_def"], strategy_option=squash_def["strategy_option"], ) - if squash_def["commit_def"]["include_in_changelog"]: - current_commits.extend( - separate_squashed_commit_def( + if ( + first_cid := f"{squash_def['commit_def']['cid']}-1" + ) in commit_cache: + raise ValueError( + f"Duplicate commit id '{first_cid}' detected!" + ) + + commit_cache.update( + { + squashed_commit_def["cid"]: squashed_commit_def + for squashed_commit_def in separate_squashed_commit_def( squashed_commit_def=squash_def["commit_def"], + parser=cast( + "SquashedCommitSupportedParser", + get_parser_from_config_file( + file=squash_def.get( + "config_file", pyproject_toml_file + ), + ), + ), ) - ) + } + ) elif action == RepoActionStep.GIT_MERGE: - this_step: RepoActionGitMerge = step_result # type: ignore[assignment] + this_step = cast("RepoActionGitMerge", step_result) with Repo(repo_dir) as git_repo: if this_step["details"]["fast_forward"]: - ff_merge_def: RepoActionGitFFMergeDetails = this_step[ # type: ignore[assignment] - "details" - ] + ff_merge_def = cast( + "RepoActionGitFFMergeDetails", this_step["details"] + ) git_repo.git.merge(ff_merge_def["branch_name"], ff=True) else: - merge_def: RepoActionGitMergeDetails = this_step[ # type: ignore[assignment] - "details" - ] + merge_def = cast( + "RepoActionGitMergeDetails", this_step["details"] + ) # Update the commit definition with the repo hash merge_def["commit_def"] = create_merge_commit( @@ -1412,8 +1595,19 @@ def _build_repo_from_definition( # noqa: C901, its required and its just test c "strategy_option", DEFAULT_MERGE_STRATEGY_OPTION ), ) - if merge_def["commit_def"]["include_in_changelog"]: - current_commits.append(merge_def["commit_def"]) + + if merge_def["commit_def"]["cid"] in commit_cache: + raise ValueError( + f"Duplicate commit id '{merge_def['commit_def']['cid']}' detected!" + ) + + commit_cache.update( + { + merge_def["commit_def"]["cid"]: merge_def[ + "commit_def" + ] + } + ) else: raise ValueError(f"Unknown action: {action}") @@ -1445,10 +1639,15 @@ def _get_cfg_value_from_def( @pytest.fixture(scope="session") -def get_versions_from_repo_build_def() -> GetVersionsFromRepoBuildDefFn: - def _get_versions(repo_def: Sequence[RepoActions]) -> Sequence[str]: +def get_versions_from_repo_build_def( + default_tag_format_str: str, +) -> GetVersionsFromRepoBuildDefFn: + def _get_versions(repo_def: Sequence[RepoActions]) -> Sequence[Version]: return [ - step["details"]["version"] + Version.parse( + step["details"]["version"], + tag_format=step["details"].get("tag_format", default_tag_format_str), + ) for step in repo_def if step["action"] == RepoActionStep.RELEASE ] @@ -1518,26 +1717,26 @@ def split_repo_actions_by_release_tags( ) -> SplitRepoActionsByReleaseTagsFn: def _split_repo_actions_by_release_tags( repo_definition: Sequence[RepoActions], - tag_format_str: str, - ) -> dict[str, list[RepoActions]]: - releasetags_2_steps: dict[str, list[RepoActions]] = { - "": [], + ) -> dict[Version | Literal["Unreleased"] | None, list[RepoActions]]: + releasetags_2_steps: dict[ + Version | Literal["Unreleased"] | None, list[RepoActions] + ] = { + None: [], } # Create generator for next release tags next_release_tag_gen = ( - tag_format_str.format(version=version) - for version in get_versions_from_repo_build_def(repo_definition) + version for version in get_versions_from_repo_build_def(repo_definition) ) # initialize the first release tag - curr_release_tag = next(next_release_tag_gen) + curr_release_tag: Version | Literal["Unreleased"] = next(next_release_tag_gen) releasetags_2_steps[curr_release_tag] = [] # Loop through all actions and split them by release tags for step in repo_definition: - if step["action"] == RepoActionStep.CONFIGURE: - releasetags_2_steps[""].append(step) + if any(step["action"] == action for action in [RepoActionStep.CONFIGURE]): + releasetags_2_steps[None].append(step) continue if step["action"] == RepoActionStep.WRITE_CHANGELOGS: @@ -1553,19 +1752,16 @@ def _split_repo_actions_by_release_tags( curr_release_tag = "Unreleased" releasetags_2_steps[curr_release_tag] = [] - # Run filter on any non-action steps of Unreleased - releasetags_2_steps["Unreleased"] = list( - filter( - lambda step: step["action"] != RepoActionStep.GIT_CHECKOUT, - releasetags_2_steps["Unreleased"], - ) - ) + insignificant_actions = [ + RepoActionStep.GIT_CHECKOUT, + ] - # Remove Unreleased if there are no steps in an Unreleased section - if ( - "Unreleased" in releasetags_2_steps - and not releasetags_2_steps["Unreleased"] - ): + # Remove Unreleased if there are no significant steps in an Unreleased section + if "Unreleased" in releasetags_2_steps and not [ + step + for step in releasetags_2_steps["Unreleased"] + if step["action"] not in insignificant_actions + ]: del releasetags_2_steps["Unreleased"] # Return all actions split up by release tags @@ -1883,7 +2079,7 @@ def _mimic_semantic_release_default_changelog( repo_definition: RepoDefinition, hvcs: Github | Gitlab | Gitea | Bitbucket, dest_file: Path | None = None, - max_version: str | None = None, + max_version: Version | Literal["Unreleased"] | None = None, output_format: ChangelogOutputFormat = ChangelogOutputFormat.MARKDOWN, mask_initial_release: bool = True, # Default as of v10 ) -> str: @@ -1926,7 +2122,7 @@ def _mimic_semantic_release_default_changelog( reduce_repo_def, # type: ignore[arg-type] repo_definition.items(), { - "version_limit": Version.parse(max_version), + "version_limit": max_version, "repo_def": {}, }, )["repo_def"] diff --git a/tests/unit/semantic_release/cli/test_util.py b/tests/unit/semantic_release/cli/test_util.py index f5637329d..c182a22f0 100644 --- a/tests/unit/semantic_release/cli/test_util.py +++ b/tests/unit/semantic_release/cli/test_util.py @@ -2,6 +2,7 @@ import json from textwrap import dedent +from typing import TYPE_CHECKING import pytest from pytest_lazy_fixtures.lazy_fixture import lf as lazy_fixture @@ -9,6 +10,10 @@ from semantic_release.cli.util import load_raw_config_file, parse_toml from semantic_release.errors import InvalidConfiguration +if TYPE_CHECKING: + from pathlib import Path + from typing import Any + @pytest.mark.parametrize( "toml_text, expected", @@ -45,7 +50,7 @@ def = 456 ), ], ) -def test_parse_toml(toml_text, expected): +def test_parse_toml(toml_text: str, expected: dict[str, Any]): assert parse_toml(toml_text) == expected @@ -62,7 +67,7 @@ def test_parse_toml_raises_invalid_configuration_with_invalid_toml(): @pytest.fixture -def raw_toml_config_file(tmp_path): +def raw_toml_config_file(tmp_path: Path) -> Path: path = tmp_path / "config.toml" path.write_text( @@ -81,10 +86,10 @@ def raw_toml_config_file(tmp_path): @pytest.fixture -def raw_pyproject_toml_config_file(tmp_path): +def raw_pyproject_toml_config_file(tmp_path: Path, pyproject_toml_file: Path) -> Path: tmp_path.mkdir(exist_ok=True) - path = tmp_path / "pyproject.toml" + path = tmp_path / pyproject_toml_file path.write_text( dedent( @@ -102,7 +107,7 @@ def raw_pyproject_toml_config_file(tmp_path): @pytest.fixture -def raw_json_config_file(tmp_path): +def raw_json_config_file(tmp_path: Path) -> Path: tmp_path.mkdir(exist_ok=True) path = tmp_path / ".releaserc" @@ -117,7 +122,7 @@ def raw_json_config_file(tmp_path): @pytest.fixture -def invalid_toml_config_file(tmp_path): +def invalid_toml_config_file(tmp_path: Path) -> Path: path = tmp_path / "config.toml" path.write_text( @@ -136,7 +141,7 @@ def invalid_toml_config_file(tmp_path): @pytest.fixture -def invalid_json_config_file(tmp_path): +def invalid_json_config_file(tmp_path: Path) -> Path: tmp_path.mkdir(exist_ok=True) path = tmp_path / "releaserc.json" @@ -153,7 +158,7 @@ def invalid_json_config_file(tmp_path): @pytest.fixture -def invalid_other_config_file(tmp_path): +def invalid_other_config_file(tmp_path: Path) -> Path: # e.g. XML path = tmp_path / "config.xml" @@ -190,7 +195,9 @@ def invalid_other_config_file(tmp_path): ), ], ) -def test_load_raw_config_file_loads_config(raw_config_file, expected): +def test_load_raw_config_file_loads_config( + raw_config_file: Path, expected: dict[str, Any] +): assert load_raw_config_file(raw_config_file) == expected @@ -202,6 +209,6 @@ def test_load_raw_config_file_loads_config(raw_config_file, expected): lazy_fixture(invalid_other_config_file.__name__), ], ) -def test_load_raw_invalid_config_file_raises_error(raw_config_file): +def test_load_raw_invalid_config_file_raises_error(raw_config_file: Path): with pytest.raises(InvalidConfiguration): load_raw_config_file(raw_config_file) diff --git a/tests/util.py b/tests/util.py index 9c884c50b..8d529a769 100644 --- a/tests/util.py +++ b/tests/util.py @@ -152,7 +152,15 @@ def shortuid(length: int = 8) -> str: def add_text_to_file(repo: Repo, filename: str, text: str | None = None): """Makes a deterministic file change for testing""" - tgt_file = Path(repo.working_tree_dir or ".") / filename + tgt_file = Path(filename).resolve().absolute() + + # TODO: switch to Path.is_relative_to() when 3.8 support is deprecated + # if not tgt_file.is_relative_to(Path(repo.working_dir).resolve().absolute()): + if Path(repo.working_dir).resolve().absolute() not in tgt_file.parents: + raise ValueError( + f"File {tgt_file} is not relative to the repository working directory {repo.working_dir}" + ) + tgt_file.parent.mkdir(parents=True, exist_ok=True) file_contents = tgt_file.read_text() if tgt_file.exists() else "" line_number = len(file_contents.splitlines()) @@ -160,7 +168,7 @@ def add_text_to_file(repo: Repo, filename: str, text: str | None = None): file_contents += f"{line_number} {text or 'default text'}{os.linesep}" tgt_file.write_text(file_contents, encoding="utf-8") - repo.index.add(filename) + repo.index.add(tgt_file) def flatten_dircmp(dcmp: filecmp.dircmp) -> list[str]: From 6928c178424b864528f8ffe333f6b16d4650f171 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 1 Sep 2025 18:31:50 -0600 Subject: [PATCH 3/7] test(fixtures): refactor normal repos to match new e2e infrastructure --- tests/fixtures/example_project.py | 38 +- .../git_flow/repo_w_1_release_channel.py | 549 +++++++++++------- .../git_flow/repo_w_2_release_channels.py | 465 ++++++++++----- .../git_flow/repo_w_3_release_channels.py | 516 ++++++++++------ .../git_flow/repo_w_4_release_channels.py | 525 ++++++++++------- .../github_flow/repo_w_default_release.py | 84 ++- ...w_default_release_w_branch_update_merge.py | 277 ++++++--- .../github_flow/repo_w_release_channels.py | 194 ++++--- tests/fixtures/repos/repo_initial_commit.py | 32 +- .../repo_w_dual_version_support.py | 66 ++- ...po_w_dual_version_support_w_prereleases.py | 86 ++- .../repos/trunk_based_dev/repo_w_no_tags.py | 39 +- .../trunk_based_dev/repo_w_prereleases.py | 66 ++- .../repos/trunk_based_dev/repo_w_tags.py | 53 +- 14 files changed, 2006 insertions(+), 984 deletions(-) diff --git a/tests/fixtures/example_project.py b/tests/fixtures/example_project.py index 8cad54dc5..6b804fdae 100644 --- a/tests/fixtures/example_project.py +++ b/tests/fixtures/example_project.py @@ -121,6 +121,9 @@ def __call__( self, file: Path | str = ... ) -> CommitParser[ParseResult, ParserOptions]: ... + class GetExpectedVersionPyFileContentFn(Protocol): + def __call__(self, version: Version | str) -> str: ... + @pytest.fixture(scope="session") def deps_files_4_example_project() -> list[Path]: @@ -478,19 +481,38 @@ def example_project_template_dir( @pytest.fixture(scope="session") -def update_version_py_file(version_py_file: Path) -> UpdateVersionPyFileFn: +def get_expected_version_py_file_content() -> GetExpectedVersionPyFileContentFn: + def _get_expected_version_py_file_content(version: Version | str) -> str: + return dedent( + f"""\ + __version__ = "{version}" + """ + ) + + return _get_expected_version_py_file_content + + +@pytest.fixture(scope="session") +def update_version_py_file( + version_py_file: Path, + get_expected_version_py_file_content: GetExpectedVersionPyFileContentFn, +) -> UpdateVersionPyFileFn: + """ + Updates the specified file with the expected version string content + + :param version: The version to set in the file + :type version: Version | str + + :param version_file: The file to update + :type version_file: Path | str + """ + def _update_version_py_file( version: Version | str, version_file: Path | str = version_py_file ) -> None: cwd_version_py = Path(version_file).resolve() cwd_version_py.parent.mkdir(parents=True, exist_ok=True) - cwd_version_py.write_text( - dedent( - f"""\ - __version__ = "{version}" - """ - ) - ) + cwd_version_py.write_text(get_expected_version_py_file_content(version)) return _update_version_py_file diff --git a/tests/fixtures/repos/git_flow/repo_w_1_release_channel.py b/tests/fixtures/repos/git_flow/repo_w_1_release_channel.py index 91eb5ffa5..4c12f6a72 100644 --- a/tests/fixtures/repos/git_flow/repo_w_1_release_channel.py +++ b/tests/fixtures/repos/git_flow/repo_w_1_release_channel.py @@ -3,11 +3,12 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.version.version import Version import tests.conftest import tests.const @@ -20,7 +21,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Generator, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -39,7 +48,9 @@ ExProjectGitRepoFn, FormatGitMergeCommitMsgFn, GetRepoDefinitionFn, + RepoActionGitFFMergeDetails, RepoActionGitMerge, + RepoActionGitMergeDetails, RepoActions, RepoActionWriteChangelogsDestFile, TomlSerializableTypes, @@ -88,6 +99,10 @@ def get_repo_definition_4_git_flow_repo_w_1_release_channels( changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, + default_tag_format_str: str, ) -> GetRepoDefinitionFn: """ This fixture returns a function that when called will define the actions needed to @@ -95,8 +110,13 @@ def get_repo_definition_4_git_flow_repo_w_1_release_channels( with a single release channel 1. official (production) releases (x.x.x) """ + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } - def _get_repo_from_defintion( + def _get_repo_from_definition( commit_type: CommitConvention, hvcs_client_name: str = "github", hvcs_domain: str = EXAMPLE_HVCS_DOMAIN, @@ -110,59 +130,70 @@ def _get_repo_from_defintion( (stable_now_datetime + timedelta(seconds=i)).isoformat(timespec="seconds") for i in count(step=1) ) + commit_parser = cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ) # Common static actions or components changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ] + ff_default_branch_merge_def: RepoActionGitMerge[RepoActionGitFFMergeDetails] = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": DEFAULT_BRANCH_NAME, + "fast_forward": True, + }, + } + fast_forward_dev_branch_actions: Sequence[RepoActions] = [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, + ff_default_branch_merge_def, + ] + + merge_dev_into_main_gen: Generator[ + RepoActionGitMerge[RepoActionGitMergeDetails], None, None + ] = ( { "action": RepoActionStep.GIT_MERGE, "details": { - "branch_name": DEFAULT_BRANCH_NAME, - "fast_forward": True, + "branch_name": DEV_BRANCH_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": f"merge-dev2main-{i}", + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=DEV_BRANCH_NAME, + tgt_branch_name=DEFAULT_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), }, - }, - ] - - merge_dev_into_main: RepoActionGitMerge = { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": DEV_BRANCH_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=DEV_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=DEV_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=DEV_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - } + } + for i in count(start=1) + ) # Define All the steps required to create the repository repo_construction_steps: list[RepoActions] = [] @@ -174,7 +205,7 @@ def _get_repo_from_defintion( "commit_type": commit_type, "hvcs_client_name": hvcs_client_name, "hvcs_domain": hvcs_domain, - "tag_format_str": tag_format_str, + "tag_format_str": tag_format_str or default_tag_format_str, "mask_initial_release": mask_initial_release, "extra_configs": { # Set the default release branch @@ -184,6 +215,7 @@ def _get_repo_from_defintion( }, "tool.semantic_release.allow_zero_version": True, "tool.semantic_release.major_on_zero": True, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, **(extra_configs or {}), }, }, @@ -191,7 +223,10 @@ def _get_repo_from_defintion( ) # Make initial release - new_version = "0.1.0" + new_version = Version.parse( + "0.1.0", tag_format=tag_format_str or default_tag_format_str + ) + repo_construction_steps.extend( [ { @@ -201,6 +236,7 @@ def _get_repo_from_defintion( # only one commit to start the main branch convert_commit_spec_to_commit_def( { + "cid": (cid_c1_initial := "c1_initial_commit"), "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -210,6 +246,7 @@ def _get_repo_from_defintion( ), }, commit_type, + parser=commit_parser, ), ], }, @@ -238,6 +275,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb1_c1_feat := "feat_branch_1_c1_feat" + ), "conventional": "feat: add new feature", "emoji": ":sparkles: add new feature", "scipy": "ENH: add new feature", @@ -246,50 +286,57 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder: RepoActionGitMerge[RepoActionGitMergeDetails] = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_1_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb1_merge := "feat_branch_1_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_1_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_1_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_dev_into_main, + **(merge_dev_into_main_1 := next(merge_dev_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -297,6 +344,14 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_c1_initial, + cid_feb1_c1_feat, + cid_feb1_merge, + merge_dev_into_main_1["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -306,7 +361,8 @@ def _get_repo_from_defintion( ) # Add a feature and officially release it - new_version = "0.2.0" + new_version = Version.parse("0.2.0", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ *fast_forward_dev_branch_actions, @@ -325,6 +381,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb2_c1_feat := "feat_branch_2_c1_feat" + ), "conventional": "feat: add a new feature", "emoji": ":sparkles: add a new feature", "scipy": "ENH: add a new feature", @@ -333,50 +392,57 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_2_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb2_merge := "feat_branch_2_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_2_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_2_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_dev_into_main, + **(merge_dev_into_main_2 := next(merge_dev_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -384,6 +450,13 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb2_c1_feat, + cid_feb2_merge, + merge_dev_into_main_2["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -393,7 +466,8 @@ def _get_repo_from_defintion( ) # Add a breaking change feature and officially release it - new_version = "1.0.0" + new_version = Version.parse("1.0.0", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ *fast_forward_dev_branch_actions, @@ -412,6 +486,10 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb3_c1_break_feat + := "feat_branch_3_c1_breaking_feature" + ), "conventional": str.join( "\n\n", [ @@ -438,50 +516,57 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_3_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb3_merge := "feat_branch_3_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_3_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_3_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_3_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_3_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_3_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_dev_into_main, + **(merge_dev_into_main_3 := next(merge_dev_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -489,6 +574,13 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb3_c1_break_feat, + cid_feb3_merge, + merge_dev_into_main_3["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -498,7 +590,8 @@ def _get_repo_from_defintion( ) # Make a fix and officially release - new_version = "1.0.1" + new_version = Version.parse("1.0.1", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ *fast_forward_dev_branch_actions, @@ -517,6 +610,7 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_fib1_c1_fix := "fix_branch_1_c1_fix"), "conventional": "fix: correct a bug\n\nCloses: #123\n", "emoji": ":bug: correct a bug\n\nCloses: #123\n", "scipy": "BUG: correct a bug\n\nCloses: #123\n", @@ -525,50 +619,57 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FIX_BRANCH_1_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_fib1_merge := "fix_branch_1_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FIX_BRANCH_1_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FIX_BRANCH_1_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_dev_into_main, + **(merge_dev_into_main_4 := next(merge_dev_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -576,6 +677,13 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_fib1_c1_fix, + cid_fib1_merge, + merge_dev_into_main_4["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -585,7 +693,8 @@ def _get_repo_from_defintion( ) # Make a fix and Add multiple feature changes before officially releasing - new_version = "1.1.0" + new_version = Version.parse("1.1.0", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ *fast_forward_dev_branch_actions, @@ -604,6 +713,7 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_fib2_c1_fix := "fix_branch_2_c1_fix"), "conventional": "fix: correct another bug", "emoji": ":bug: correct another bug", "scipy": "BUG: correct another bug", @@ -612,39 +722,45 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FIX_BRANCH_2_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_fib2_merge := "fix_branch_2_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FIX_BRANCH_2_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FIX_BRANCH_2_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": { @@ -660,6 +776,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb4_c1_feat := "feat_branch_4_c1_feat" + ), "conventional": "feat(cli): add new config cli command", "emoji": ":sparkles: (cli) add new config cli command", "scipy": "ENH: cli: add new config cli command", @@ -668,50 +787,57 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_4_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb4_merge := "feat_branch_4_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_4_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_4_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_4_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_4_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_4_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_dev_into_main, + **(merge_dev_into_main_5 := next(merge_dev_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -719,6 +845,15 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_fib2_c1_fix, + cid_fib2_merge, + cid_feb4_c1_feat, + cid_feb4_merge, + merge_dev_into_main_5["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -729,7 +864,7 @@ def _get_repo_from_defintion( return repo_construction_steps - return _get_repo_from_defintion + return _get_repo_from_definition @pytest.fixture(scope="session") diff --git a/tests/fixtures/repos/git_flow/repo_w_2_release_channels.py b/tests/fixtures/repos/git_flow/repo_w_2_release_channels.py index 3d75af918..59aa7541a 100644 --- a/tests/fixtures/repos/git_flow/repo_w_2_release_channels.py +++ b/tests/fixtures/repos/git_flow/repo_w_2_release_channels.py @@ -3,11 +3,12 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.version.version import Version import tests.conftest import tests.const @@ -20,7 +21,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Generator, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -39,7 +48,9 @@ ExProjectGitRepoFn, FormatGitMergeCommitMsgFn, GetRepoDefinitionFn, + RepoActionGitFFMergeDetails, RepoActionGitMerge, + RepoActionGitMergeDetails, RepoActions, RepoActionWriteChangelogsDestFile, TomlSerializableTypes, @@ -87,6 +98,10 @@ def get_repo_definition_4_git_flow_repo_w_2_release_channels( changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, + default_tag_format_str: str, ) -> GetRepoDefinitionFn: """ This fixture returns a function that when called will define the actions needed to @@ -95,8 +110,13 @@ def get_repo_definition_4_git_flow_repo_w_2_release_channels( 1. alpha feature releases (x.x.x-alpha.x) 2. official (production) releases (x.x.x) """ + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } - def _get_repo_from_defintion( + def _get_repo_from_definition( commit_type: CommitConvention, hvcs_client_name: str = "github", hvcs_domain: str = EXAMPLE_HVCS_DOMAIN, @@ -110,59 +130,70 @@ def _get_repo_from_defintion( (stable_now_datetime + timedelta(seconds=i)).isoformat(timespec="seconds") for i in count(step=1) ) + commit_parser = cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ) # Common static actions or components changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ] + ff_default_branch_merge_def: RepoActionGitMerge[RepoActionGitFFMergeDetails] = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": DEFAULT_BRANCH_NAME, + "fast_forward": True, + }, + } + fast_forward_dev_branch_actions: Sequence[RepoActions] = [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, + ff_default_branch_merge_def, + ] + + merge_dev_into_main_gen: Generator[ + RepoActionGitMerge[RepoActionGitMergeDetails], None, None + ] = ( { "action": RepoActionStep.GIT_MERGE, "details": { - "branch_name": DEFAULT_BRANCH_NAME, - "fast_forward": True, + "branch_name": DEV_BRANCH_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": f"merge-dev2main-{i}", + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=DEV_BRANCH_NAME, + tgt_branch_name=DEFAULT_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), }, - }, - ] - - merge_dev_into_main: RepoActionGitMerge = { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": DEV_BRANCH_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=DEV_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=DEV_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=DEV_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - } + } + for i in count(start=1) + ) # Define All the steps required to create the repository repo_construction_steps: list[RepoActions] = [] @@ -174,7 +205,7 @@ def _get_repo_from_defintion( "commit_type": commit_type, "hvcs_client_name": hvcs_client_name, "hvcs_domain": hvcs_domain, - "tag_format_str": tag_format_str, + "tag_format_str": tag_format_str or default_tag_format_str, "mask_initial_release": mask_initial_release, "extra_configs": { # Set the default release branch @@ -190,6 +221,7 @@ def _get_repo_from_defintion( }, "tool.semantic_release.allow_zero_version": True, "tool.semantic_release.major_on_zero": True, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, **(extra_configs or {}), }, }, @@ -197,7 +229,10 @@ def _get_repo_from_defintion( ) # Make initial release - new_version = "0.1.0" + new_version = Version.parse( + "0.1.0", tag_format=tag_format_str or default_tag_format_str + ) + repo_construction_steps.extend( [ { @@ -207,6 +242,7 @@ def _get_repo_from_defintion( # only one commit to start the main branch convert_commit_spec_to_commit_def( { + "cid": (cid_c1_initial := "c1_initial_commit"), "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -216,6 +252,7 @@ def _get_repo_from_defintion( ), }, commit_type, + parser=commit_parser, ), ], }, @@ -244,6 +281,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb1_c1_feat := "feat_branch_1_c1_feat" + ), "conventional": "feat: add new feature", "emoji": ":sparkles: add new feature", "scipy": "ENH: add new feature", @@ -252,50 +292,57 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder: RepoActionGitMerge[RepoActionGitMergeDetails] = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_1_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb1_merge := "feat_branch_1_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_1_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_1_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_dev_into_main, + **(merge_dev_into_main_1 := next(merge_dev_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -303,6 +350,14 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_c1_initial, + cid_feb1_c1_feat, + cid_feb1_merge, + merge_dev_into_main_1["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -312,7 +367,7 @@ def _get_repo_from_defintion( ) # Add a feature and release it as an alpha release - new_version = "0.2.0-alpha.1" + new_version = Version.parse("0.2.0-alpha.1", tag_format=new_version.tag_format) repo_construction_steps.extend( [ *fast_forward_dev_branch_actions, @@ -331,6 +386,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb2_c1_feat := "feat_branch_2_c1_feat" + ), "conventional": "feat: add a new feature", "emoji": ":sparkles: add a new feature", "scipy": "ENH: add a new feature", @@ -339,13 +397,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -353,6 +413,9 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb2_c1_feat, + ], }, }, ], @@ -362,7 +425,8 @@ def _get_repo_from_defintion( ) # Add a feature and release it as an alpha release - new_version = "1.0.0-alpha.1" + new_version = Version.parse("1.0.0-alpha.1", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ { @@ -371,6 +435,10 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb2_c2_break_feat + := "feat_branch_2_c2_breaking_feat" + ), "conventional": str.join( "\n\n", [ @@ -397,13 +465,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -411,6 +481,9 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb2_c2_break_feat, + ], }, }, ], @@ -420,7 +493,8 @@ def _get_repo_from_defintion( ) # Add another feature and officially release - new_version = "1.0.0" + new_version = Version.parse("1.0.0", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ { @@ -429,6 +503,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb2_c3_feat := "feat_branch_2_c3_feat" + ), "conventional": "feat: add some more text", "emoji": ":sparkles: add some more text", "scipy": "ENH: add some more text", @@ -437,50 +514,57 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_2_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb2_merge := "feat_branch_2_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_2_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_2_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_dev_into_main, + **(merge_dev_into_main_1 := next(merge_dev_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -488,6 +572,13 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb2_c3_feat, + cid_feb2_merge, + merge_dev_into_main_1["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -497,7 +588,8 @@ def _get_repo_from_defintion( ) # Add another feature and officially release (no intermediate alpha release) - new_version = "1.1.0" + new_version = Version.parse("1.1.0", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ *fast_forward_dev_branch_actions, @@ -516,6 +608,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb3_c1_feat := "feat_branch_3_c1_feat" + ), "conventional": "feat(cli): add new config cli command", "emoji": ":sparkles: (cli) add new config cli command", "scipy": "ENH: cli: add new config cli command", @@ -524,50 +619,57 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_3_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb3_merge := "feat_branch_3_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_3_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_3_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_3_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_3_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_3_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_dev_into_main, + **(merge_dev_into_main_2 := next(merge_dev_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -575,6 +677,13 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb3_c1_feat, + cid_feb3_merge, + merge_dev_into_main_2["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -584,7 +693,8 @@ def _get_repo_from_defintion( ) # Make a fix and officially release - new_version = "1.1.1" + new_version = Version.parse("1.1.1", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ *fast_forward_dev_branch_actions, @@ -603,6 +713,7 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_fib1_c1_fix := "fix_branch_1_c1_fix"), "conventional": "fix(config): fixed configuration generation\n\nCloses: #123", "emoji": ":bug: (config) fixed configuration generation\n\nCloses: #123", "scipy": "MAINT:config: fixed configuration generation\n\nCloses: #123", @@ -611,50 +722,57 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FIX_BRANCH_1_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_fib1_merge := "fix_branch_1_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FIX_BRANCH_1_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FIX_BRANCH_1_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_dev_into_main, + **(merge_dev_into_main_3 := next(merge_dev_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -662,6 +780,13 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_fib1_c1_fix, + cid_fib1_merge, + merge_dev_into_main_3["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -671,7 +796,8 @@ def _get_repo_from_defintion( ) # Introduce a new feature and create a prerelease for it - new_version = "1.2.0-alpha.1" + new_version = Version.parse("1.2.0-alpha.1", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ *fast_forward_dev_branch_actions, @@ -690,6 +816,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb4_c1_feat := "feat_branch_4_c1_feat" + ), "conventional": "feat: add some more text", "emoji": ":sparkles: add some more text", "scipy": "ENH: add some more text", @@ -698,13 +827,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -712,6 +843,9 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb4_c1_feat, + ], }, }, ], @@ -721,7 +855,8 @@ def _get_repo_from_defintion( ) # Fix the previous alpha & add additional feature and create a subsequent prerelease for it - new_version = "1.2.0-alpha.2" + new_version = Version.parse("1.2.0-alpha.2", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ { @@ -730,6 +865,7 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_feb4_c2_fix := "feat_branch_4_c2_fix"), "conventional": "fix(scope): correct some text", "emoji": ":bug: (scope) correct some text", "scipy": "MAINT:scope: correct some text", @@ -737,6 +873,9 @@ def _get_repo_from_defintion( "include_in_changelog": True, }, { + "cid": ( + cid_feb4_c3_feat := "feat_branch_4_c3_feat" + ), "conventional": "feat(scope): add some more text", "emoji": ":sparkles:(scope) add some more text", "scipy": "ENH: scope: add some more text", @@ -745,13 +884,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -759,6 +900,10 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb4_c2_fix, + cid_feb4_c3_feat, + ], }, }, ], @@ -769,7 +914,7 @@ def _get_repo_from_defintion( return repo_construction_steps - return _get_repo_from_defintion + return _get_repo_from_definition @pytest.fixture(scope="session") diff --git a/tests/fixtures/repos/git_flow/repo_w_3_release_channels.py b/tests/fixtures/repos/git_flow/repo_w_3_release_channels.py index d52b60f97..cc8b6b42d 100644 --- a/tests/fixtures/repos/git_flow/repo_w_3_release_channels.py +++ b/tests/fixtures/repos/git_flow/repo_w_3_release_channels.py @@ -3,11 +3,12 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.version.version import Version import tests.conftest import tests.const @@ -20,7 +21,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Generator, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -39,7 +48,9 @@ ExProjectGitRepoFn, FormatGitMergeCommitMsgFn, GetRepoDefinitionFn, + RepoActionGitFFMergeDetails, RepoActionGitMerge, + RepoActionGitMergeDetails, RepoActions, RepoActionWriteChangelogsDestFile, TomlSerializableTypes, @@ -88,6 +99,10 @@ def get_repo_definition_4_git_flow_repo_w_3_release_channels( changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, + default_tag_format_str: str, ) -> GetRepoDefinitionFn: """ This fixture returns a function that when called will define the actions needed to @@ -97,8 +112,13 @@ def get_repo_definition_4_git_flow_repo_w_3_release_channels( 2. release candidate releases (x.x.x-rc.x) 3. official (production) releases (x.x.x) """ + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } - def _get_repo_from_defintion( + def _get_repo_from_definition( commit_type: CommitConvention, hvcs_client_name: str = "github", hvcs_domain: str = EXAMPLE_HVCS_DOMAIN, @@ -112,59 +132,70 @@ def _get_repo_from_defintion( (stable_now_datetime + timedelta(seconds=i)).isoformat(timespec="seconds") for i in count(step=1) ) + commit_parser = cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ) # Common static actions or components changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ] + ff_default_branch_merge_def: RepoActionGitMerge[RepoActionGitFFMergeDetails] = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": DEFAULT_BRANCH_NAME, + "fast_forward": True, + }, + } + fast_forward_dev_branch_actions: Sequence[RepoActions] = [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, + ff_default_branch_merge_def, + ] + + merge_dev_into_main_gen: Generator[ + RepoActionGitMerge[RepoActionGitMergeDetails], None, None + ] = ( { "action": RepoActionStep.GIT_MERGE, "details": { - "branch_name": DEFAULT_BRANCH_NAME, - "fast_forward": True, + "branch_name": DEV_BRANCH_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": f"merge-dev2main-{i}", + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=DEV_BRANCH_NAME, + tgt_branch_name=DEFAULT_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), }, - }, - ] - - merge_dev_into_main: RepoActionGitMerge = { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": DEV_BRANCH_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=DEV_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=DEV_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=DEV_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - } + } + for i in count(start=1) + ) # Define All the steps required to create the repository repo_construction_steps: list[RepoActions] = [] @@ -176,7 +207,7 @@ def _get_repo_from_defintion( "commit_type": commit_type, "hvcs_client_name": hvcs_client_name, "hvcs_domain": hvcs_domain, - "tag_format_str": tag_format_str, + "tag_format_str": tag_format_str or default_tag_format_str, "mask_initial_release": mask_initial_release, "extra_configs": { # Set the default release branch @@ -198,6 +229,7 @@ def _get_repo_from_defintion( }, "tool.semantic_release.allow_zero_version": True, "tool.semantic_release.major_on_zero": True, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, **(extra_configs or {}), }, }, @@ -205,7 +237,10 @@ def _get_repo_from_defintion( ) # Make initial release - new_version = "0.1.0" + new_version = Version.parse( + "0.1.0", tag_format=tag_format_str or default_tag_format_str + ) + repo_construction_steps.extend( [ { @@ -215,6 +250,7 @@ def _get_repo_from_defintion( # only one commit to start the main branch convert_commit_spec_to_commit_def( { + "cid": (cid_c1_initial := "c1_initial_commit"), "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -224,6 +260,7 @@ def _get_repo_from_defintion( ), }, commit_type, + parser=commit_parser, ), ], }, @@ -252,6 +289,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb1_c1_feat := "feat_branch_1_c1_feat" + ), "conventional": "feat: add new feature", "emoji": ":sparkles: add new feature", "scipy": "ENH: add new feature", @@ -260,50 +300,57 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder: RepoActionGitMerge[RepoActionGitMergeDetails] = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_1_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb1_merge := "feat_branch_1_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_1_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_1_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_dev_into_main, + **(merge_dev_into_main_1 := next(merge_dev_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -311,6 +358,14 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_c1_initial, + cid_feb1_c1_feat, + cid_feb1_merge, + merge_dev_into_main_1["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -320,7 +375,8 @@ def _get_repo_from_defintion( ) # Add a feature and release it as an alpha release - new_version = "0.2.0-alpha.1" + new_version = Version.parse("0.2.0-alpha.1", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ *fast_forward_dev_branch_actions, @@ -339,6 +395,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb2_c1_feat := "feat_branch_2_c1_feat" + ), "conventional": "feat: add a new feature", "emoji": ":sparkles: add a new feature", "scipy": "ENH: add a new feature", @@ -347,13 +406,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -361,6 +422,9 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb2_c1_feat, + ], }, }, ], @@ -370,7 +434,8 @@ def _get_repo_from_defintion( ) # Make a breaking feature change and release it as an alpha release - new_version = "1.0.0-alpha.1" + new_version = Version.parse("1.0.0-alpha.1", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ { @@ -379,6 +444,10 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb2_c2_break_feat + := "feat_branch_2_c2_break_feat" + ), "conventional": str.join( "\n\n", [ @@ -405,13 +474,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -419,6 +490,9 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb2_c2_break_feat, + ], }, }, ], @@ -428,43 +502,45 @@ def _get_repo_from_defintion( ) # Merge in the successful alpha release and create a release candidate - new_version = "1.0.0-rc.1" + new_version = Version.parse("1.0.0-rc.1", tag_format=new_version.tag_format) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_2_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb2_merge := "feat_branch_2_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_2_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + repo_construction_steps.extend( [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_2_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -472,6 +548,9 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb2_merge, + ], }, }, ], @@ -481,7 +560,8 @@ def _get_repo_from_defintion( ) # officially release the sucessful release candidate to production - new_version = "1.0.0" + new_version = Version.parse("1.0.0", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ { @@ -489,12 +569,13 @@ def _get_repo_from_defintion( "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_dev_into_main, + **(merge_dev_into_main_2 := next(merge_dev_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -502,6 +583,11 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + merge_dev_into_main_2["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -511,7 +597,8 @@ def _get_repo_from_defintion( ) # Add a feature and release it as an alpha release - new_version = "1.1.0-alpha.1" + new_version = Version.parse("1.1.0-alpha.1", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ *fast_forward_dev_branch_actions, @@ -530,6 +617,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb3_c1_feat := "feat_branch_3_c1_feat" + ), "conventional": "feat(cli): add new config cli command", "emoji": ":sparkles: (cli) add new config cli command", "scipy": "ENH:cli: add new config cli command", @@ -538,13 +628,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -552,6 +644,9 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb3_c1_feat, + ], }, }, ], @@ -561,7 +656,8 @@ def _get_repo_from_defintion( ) # Add another feature and release it as subsequent alpha release - new_version = "1.1.0-alpha.2" + new_version = Version.parse("1.1.0-alpha.2", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ { @@ -570,6 +666,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb3_c2_feat := "feat_branch_3_c2_feat" + ), "conventional": "feat(config): add new config option", "emoji": ":sparkles: (config) add new config option", "scipy": "ENH: config: add new config option", @@ -578,13 +677,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -592,6 +693,9 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb3_c2_feat, + ], }, }, ], @@ -601,39 +705,40 @@ def _get_repo_from_defintion( ) # Merge in the successful alpha release, add a fix, and create a release candidate - new_version = "1.1.0-rc.1" + new_version = Version.parse("1.1.0-rc.1", tag_format=new_version.tag_format) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_3_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb3_merge2dev := "feat_branch_3_merge2dev"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_3_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + repo_construction_steps.extend( [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_3_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_3_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_3_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_3_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": { @@ -649,6 +754,7 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_fib1_c1_fix := "fix_branch_1_c1_fix"), "conventional": "fix(cli): fix config cli command", "emoji": ":bug: (cli) fix config cli command", "scipy": "BUG:cli: fix config cli command", @@ -657,43 +763,50 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FIX_BRANCH_1_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_fib1_merge2dev := "fix_branch_1_merge2dev"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FIX_BRANCH_1_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FIX_BRANCH_1_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -701,6 +814,11 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb3_merge2dev, + cid_fib1_c1_fix, + cid_fib1_merge2dev, + ], }, }, ], @@ -710,7 +828,8 @@ def _get_repo_from_defintion( ) # fix another bug from the release candidate and create a new release candidate - new_version = "1.1.0-rc.2" + new_version = Version.parse("1.1.0-rc.2", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ { @@ -728,6 +847,7 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_fib2_c1_fix := "fix_branch_2_c1"), "conventional": "fix(config): fix config option\n\nImplements: #123\n", "emoji": ":bug: (config) fix config option\n\nImplements: #123\n", "scipy": "BUG: config: fix config option\n\nImplements: #123\n", @@ -736,43 +856,50 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FIX_BRANCH_2_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_fib2_merge2dev := "fix_branch_2_merge2dev"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FIX_BRANCH_2_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FIX_BRANCH_2_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -780,6 +907,10 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_fib2_c1_fix, + cid_fib2_merge2dev, + ], }, }, ], @@ -789,7 +920,8 @@ def _get_repo_from_defintion( ) # officially release the sucessful release candidate to production - new_version = "1.1.0" + new_version = Version.parse("1.1.0", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ { @@ -797,12 +929,13 @@ def _get_repo_from_defintion( "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_dev_into_main, + **(merge_dev_into_main_3 := next(merge_dev_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -810,6 +943,11 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + merge_dev_into_main_3["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -820,7 +958,7 @@ def _get_repo_from_defintion( return repo_construction_steps - return _get_repo_from_defintion + return _get_repo_from_definition @pytest.fixture(scope="session") diff --git a/tests/fixtures/repos/git_flow/repo_w_4_release_channels.py b/tests/fixtures/repos/git_flow/repo_w_4_release_channels.py index 5bdb76d7d..4cb3ac1f4 100644 --- a/tests/fixtures/repos/git_flow/repo_w_4_release_channels.py +++ b/tests/fixtures/repos/git_flow/repo_w_4_release_channels.py @@ -3,11 +3,12 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.version.version import Version import tests.conftest import tests.const @@ -20,7 +21,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Generator, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -39,7 +48,9 @@ ExProjectGitRepoFn, FormatGitMergeCommitMsgFn, GetRepoDefinitionFn, + RepoActionGitFFMergeDetails, RepoActionGitMerge, + RepoActionGitMergeDetails, RepoActions, RepoActionWriteChangelogsDestFile, TomlSerializableTypes, @@ -89,6 +100,10 @@ def get_repo_definition_4_git_flow_repo_w_4_release_channels( changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, + default_tag_format_str: str, ) -> GetRepoDefinitionFn: """ This fixture returns a function that when called will define the actions needed to @@ -107,8 +122,13 @@ def get_repo_definition_4_git_flow_repo_w_4_release_channels( 4. [main branch] official (production) releases (x.x.x) """ + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } - def _get_repo_from_defintion( + def _get_repo_from_definition( commit_type: CommitConvention, hvcs_client_name: str = "github", hvcs_domain: str = EXAMPLE_HVCS_DOMAIN, @@ -122,100 +142,116 @@ def _get_repo_from_defintion( (stable_now_datetime + timedelta(seconds=i)).isoformat(timespec="seconds") for i in count(step=1) ) + commit_parser = cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ) # Common static actions or components changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ] + ff_beta_branch_merge_def: RepoActionGitMerge[RepoActionGitFFMergeDetails] = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": BETA_BRANCH_NAME, + "fast_forward": True, + }, + } + fast_forward_dev_branch_actions: Sequence[RepoActions] = [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": BETA_BRANCH_NAME, - "fast_forward": True, - }, - }, + ff_beta_branch_merge_def, ] + ff_main_branch_merge_def: RepoActionGitMerge[RepoActionGitFFMergeDetails] = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": DEFAULT_BRANCH_NAME, + "fast_forward": True, + }, + } + fast_forward_beta_branch_actions: Sequence[RepoActions] = [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": BETA_BRANCH_NAME}, }, + ff_main_branch_merge_def, + ] + + merge_dev_into_beta_gen: Generator[ + RepoActionGitMerge[RepoActionGitMergeDetails], None, None + ] = ( { "action": RepoActionStep.GIT_MERGE, "details": { - "branch_name": DEFAULT_BRANCH_NAME, - "fast_forward": True, + "branch_name": DEV_BRANCH_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": f"merge-dev2beta-{i}", + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=DEV_BRANCH_NAME, + tgt_branch_name=BETA_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), }, - }, - ] - - merge_dev_into_beta: RepoActionGitMerge = { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": DEV_BRANCH_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=DEV_BRANCH_NAME, - tgt_branch_name=BETA_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=DEV_BRANCH_NAME, - tgt_branch_name=BETA_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=DEV_BRANCH_NAME, - tgt_branch_name=BETA_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - } + } + for i in count(start=1) + ) - merge_beta_into_main: RepoActionGitMerge = { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": BETA_BRANCH_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=BETA_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=BETA_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=BETA_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - } + merge_beta_into_main_gen: Generator[ + RepoActionGitMerge[RepoActionGitMergeDetails], None, None + ] = ( + { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": BETA_BRANCH_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": f"merge-beta2main-{i}", + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=BETA_BRANCH_NAME, + tgt_branch_name=DEFAULT_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + for i in count(start=1) + ) # Define All the steps required to create the repository repo_construction_steps: list[RepoActions] = [] @@ -227,7 +263,7 @@ def _get_repo_from_defintion( "commit_type": commit_type, "hvcs_client_name": hvcs_client_name, "hvcs_domain": hvcs_domain, - "tag_format_str": tag_format_str, + "tag_format_str": tag_format_str or default_tag_format_str, "mask_initial_release": mask_initial_release, "extra_configs": { # Set the default release branch @@ -254,6 +290,7 @@ def _get_repo_from_defintion( "prerelease_token": "rev", }, "tool.semantic_release.allow_zero_version": False, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, **(extra_configs or {}), }, }, @@ -261,7 +298,10 @@ def _get_repo_from_defintion( ) # Make initial release - new_version = "1.0.0" + new_version = Version.parse( + "1.0.0", tag_format=(tag_format_str or default_tag_format_str) + ) + repo_construction_steps.extend( [ { @@ -271,6 +311,9 @@ def _get_repo_from_defintion( # only one commit to start the main branch convert_commit_spec_to_commit_def( { + "cid": ( + cid_db_c1_initial := "db_c1_initial_commit" + ), "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -280,6 +323,7 @@ def _get_repo_from_defintion( ), }, commit_type, + parser=commit_parser, ), ], }, @@ -317,6 +361,7 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_feb1_c1_feat := "feat_branch1_c1_feat"), "conventional": "feat: add new feature", "emoji": ":sparkles: add new feature", "scipy": "ENH: add new feature", @@ -325,57 +370,64 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder: RepoActionGitMerge[RepoActionGitMergeDetails] = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_1_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb1_merge2dev := "feat_branch1_merge2dev"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_1_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_1_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": BETA_BRANCH_NAME}, }, { - **merge_dev_into_beta, + **(merge_dev_into_beta_1 := next(merge_dev_into_beta_gen)), }, { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_beta_into_main, + **(merge_beta_into_main_1 := next(merge_beta_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -383,6 +435,17 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_db_c1_initial, + cid_feb1_c1_feat, + cid_feb1_merge2dev, + merge_dev_into_beta_1["details"]["commit_def"][ + "cid" + ], + merge_beta_into_main_1["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -392,7 +455,7 @@ def _get_repo_from_defintion( ) # Make a fix and release it as an alpha release - new_version = "1.0.1-alpha.1" + new_version = Version.parse("1.0.1-alpha.1", tag_format=new_version.tag_format) repo_construction_steps.extend( [ *fast_forward_beta_branch_actions, @@ -412,6 +475,7 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_fib1_c1_fix := "fix_branch_1_c1_fix"), "conventional": "fix(cli): fix config cli command\n\nCloses: #123\n", "emoji": ":bug: (cli) fix config cli command\n\nCloses: #123\n", "scipy": "BUG:cli: fix config cli command\n\nCloses: #123\n", @@ -420,43 +484,50 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FIX_BRANCH_1_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_fib1_merge2dev := "fix_branch_1_merge_2_dev"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FIX_BRANCH_1_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FIX_BRANCH_1_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_1_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -464,6 +535,10 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_fib1_c1_fix, + cid_fib1_merge2dev, + ], }, }, ], @@ -473,7 +548,7 @@ def _get_repo_from_defintion( ) # Merge in the successful alpha release and create a beta release - new_version = "1.0.1-beta.1" + new_version = Version.parse("1.0.1-beta.1", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -481,12 +556,13 @@ def _get_repo_from_defintion( "details": {"branch": BETA_BRANCH_NAME}, }, { - **merge_dev_into_beta, + **(merge_dev_into_beta_2 := next(merge_dev_into_beta_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -494,6 +570,11 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + merge_dev_into_beta_2["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -503,7 +584,7 @@ def _get_repo_from_defintion( ) # Fix a bug found in beta release and create a new alpha release - new_version = "1.0.1-alpha.2" + new_version = Version.parse("1.0.1-alpha.2", tag_format=new_version.tag_format) repo_construction_steps.extend( [ *fast_forward_dev_branch_actions, @@ -522,6 +603,7 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_fib2_c1_fix := "fix_branch_2_c1_fix"), "conventional": "fix(config): fix config option", "emoji": ":bug: (config) fix config option", "scipy": "BUG: config: fix config option", @@ -530,43 +612,50 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FIX_BRANCH_2_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_fib2_merge2dev := "fix_branch_2_merge_2_dev"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FIX_BRANCH_2_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FIX_BRANCH_2_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FIX_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -574,6 +663,10 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_fib2_c1_fix, + cid_fib2_merge2dev, + ], }, }, ], @@ -583,7 +676,8 @@ def _get_repo_from_defintion( ) # Merge in the 2nd successful alpha release and create a secondary beta release - new_version = "1.0.1-beta.2" + new_version = Version.parse("1.0.1-beta.2", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ { @@ -591,12 +685,13 @@ def _get_repo_from_defintion( "details": {"branch": BETA_BRANCH_NAME}, }, { - **merge_dev_into_beta, + **(merge_dev_into_beta_3 := next(merge_dev_into_beta_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -604,6 +699,11 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + merge_dev_into_beta_3["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -614,7 +714,11 @@ def _get_repo_from_defintion( # Add a new feature (another developer was working on) and create a release for it # Based on Semver standard, Build metadata is restricted to [A-Za-z0-9-] so we replace the '/' with a '-' - new_version = f"""1.1.0-rev.1+{FEAT_BRANCH_2_NAME.replace("/", '-')}""" + new_version = Version.parse( + f"""1.1.0-rev.1+{FEAT_BRANCH_2_NAME.replace("/", '-')}""", + tag_format=new_version.tag_format, + ) + repo_construction_steps.extend( [ *fast_forward_dev_branch_actions, @@ -633,6 +737,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb2_c1_feat := "feat_branch_2_c1_feat" + ), "conventional": "feat(feat-2): add another primary feature", "emoji": ":sparkles: (feat-2) add another primary feature", "scipy": "ENH: feat-2: add another primary feature", @@ -641,13 +748,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -655,6 +764,9 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb2_c1_feat, + ], }, }, ], @@ -664,43 +776,45 @@ def _get_repo_from_defintion( ) # Merge in the successful revision release and create an alpha release - new_version = "1.1.0-alpha.1" + new_version = Version.parse("1.1.0-alpha.1", tag_format=new_version.tag_format) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_2_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb2_merge2dev := "feat_branch_2_merge_2_dev"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_2_NAME, + tgt_branch_name=DEV_BRANCH_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + repo_construction_steps.extend( [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEV_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_2_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_2_NAME, - tgt_branch_name=DEV_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -708,6 +822,9 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb2_merge2dev, + ], }, }, ], @@ -717,7 +834,8 @@ def _get_repo_from_defintion( ) # Merge in the successful alpha release and create a beta release - new_version = "1.1.0-beta.1" + new_version = Version.parse("1.1.0-beta.1", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ { @@ -725,12 +843,13 @@ def _get_repo_from_defintion( "details": {"branch": BETA_BRANCH_NAME}, }, { - **merge_dev_into_beta, + **(merge_dev_into_beta_4 := next(merge_dev_into_beta_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -738,6 +857,11 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + merge_dev_into_beta_4["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -747,7 +871,8 @@ def _get_repo_from_defintion( ) # officially release the sucessful release candidate to production - new_version = "1.1.0" + new_version = Version.parse("1.1.0", tag_format=new_version.tag_format) + repo_construction_steps.extend( [ { @@ -755,12 +880,13 @@ def _get_repo_from_defintion( "details": {"branch": DEFAULT_BRANCH_NAME}, }, { - **merge_beta_into_main, + **(merge_beta_into_main_2 := next(merge_beta_into_main_gen)), }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -768,6 +894,11 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + merge_beta_into_main_2["details"]["commit_def"][ + "cid" + ], + ], }, }, ], @@ -778,7 +909,7 @@ def _get_repo_from_defintion( return repo_construction_steps - return _get_repo_from_defintion + return _get_repo_from_definition @pytest.fixture(scope="session") diff --git a/tests/fixtures/repos/github_flow/repo_w_default_release.py b/tests/fixtures/repos/github_flow/repo_w_default_release.py index 61816f0bf..612dac16f 100644 --- a/tests/fixtures/repos/github_flow/repo_w_default_release.py +++ b/tests/fixtures/repos/github_flow/repo_w_default_release.py @@ -3,11 +3,12 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.version.version import Version import tests.conftest import tests.const @@ -20,7 +21,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -85,11 +94,20 @@ def get_repo_definition_4_github_flow_repo_w_default_release_channel( changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, + default_tag_format_str: str, ) -> GetRepoDefinitionFn: """ Builds a repository with the GitHub Flow branching strategy and a squash commit merging strategy for a single release channel on the default branch. """ + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } def _get_repo_from_definition( commit_type: CommitConvention, @@ -106,15 +124,21 @@ def _get_repo_from_definition( for i in count(step=1) ) pr_num_gen = (i for i in count(start=2, step=1)) + commit_parser = cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ) changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ] @@ -127,7 +151,7 @@ def _get_repo_from_definition( "commit_type": commit_type, "hvcs_client_name": hvcs_client_name, "hvcs_domain": hvcs_domain, - "tag_format_str": tag_format_str, + "tag_format_str": tag_format_str or default_tag_format_str, "mask_initial_release": mask_initial_release, "extra_configs": { # Set the default release branch @@ -137,13 +161,16 @@ def _get_repo_from_definition( }, "tool.semantic_release.allow_zero_version": False, "tool.semantic_release.commit_parser_options.parse_squash_commits": True, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, **(extra_configs or {}), }, }, } ) - new_version = "1.0.0" + new_version = Version.parse( + "1.0.0", tag_format=(tag_format_str or default_tag_format_str) + ) repo_construction_steps.extend( [ @@ -153,6 +180,9 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_db_c1_initial := "db_c1_initial_commit" + ), "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -162,6 +192,7 @@ def _get_repo_from_definition( ), }, { + "cid": (cid_db_c2_feat := "db_c2_feat"), "conventional": "feat: add new feature", "emoji": ":sparkles: add new feature", "scipy": "ENH: add new feature", @@ -170,13 +201,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -184,6 +217,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_db_c1_initial, cid_db_c2_feat], }, }, ], @@ -194,6 +228,7 @@ def _get_repo_from_definition( fix_branch_1_commits: Sequence[CommitSpec] = [ { + "cid": "fix_branch_c1", "conventional": "fix(cli): add missing text\n\nResolves: #123\n", "emoji": ":bug: add missing text\n\nResolves: #123\n", "scipy": "MAINT: add missing text\n\nResolves: #123\n", @@ -224,6 +259,7 @@ def _get_repo_from_definition( for commit in fix_branch_1_commits ], commit_type, + parser=commit_parser, ), }, }, @@ -233,18 +269,21 @@ def _get_repo_from_definition( # simulate separate work by another person at same time as the fix branch feat_branch_1_commits: Sequence[CommitSpec] = [ { + "cid": "feat_branch_c1", "conventional": "feat(cli): add cli interface", "emoji": ":sparkles: add cli interface", "scipy": "ENH: add cli interface", "datetime": next(commit_timestamp_gen), }, { + "cid": "feat_branch_c2", "conventional": "test(cli): add cli tests", "emoji": ":checkmark: add cli tests", "scipy": "TST: add cli tests", "datetime": next(commit_timestamp_gen), }, { + "cid": "feat_branch_c3", "conventional": "docs(cli): add cli documentation", "emoji": ":memo: add cli documentation", "scipy": "DOC: add cli documentation", @@ -275,16 +314,21 @@ def _get_repo_from_definition( for commit in feat_branch_1_commits ], commit_type, + parser=commit_parser, ) }, }, ] ) - new_version = "1.0.1" + new_version = Version.parse("1.0.1", tag_format=new_version.tag_format) all_commit_types: list[CommitConvention] = ["conventional", "emoji", "scipy"] fix_branch_pr_number = next(pr_num_gen) + cid_fix_branch_squash_base = "fix_branch_1_squash" + cid_fix_branch_squash_gen = ( + f"{cid_fix_branch_squash_base}-{i}" for i in count(start=1) + ) fix_branch_squash_commit_spec: CommitSpec = { **{ # type: ignore[typeddict-item] cmt_type: format_squash_commit_msg_github( @@ -292,10 +336,13 @@ def _get_repo_from_definition( pr_title=fix_branch_1_commits[0][cmt_type], pr_number=fix_branch_pr_number, # No squashed commits since there is only one commit - squashed_commits=[], + squashed_commits=[ + cmt[commit_type] for cmt in fix_branch_1_commits[1:] + ], ) for cmt_type in all_commit_types }, + "cid": cid_fix_branch_squash_base, "datetime": next(commit_timestamp_gen), "include_in_changelog": True, } @@ -314,13 +361,15 @@ def _get_repo_from_definition( "commit_def": convert_commit_spec_to_commit_def( fix_branch_squash_commit_spec, commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -328,6 +377,10 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + next(cid_fix_branch_squash_gen) + for _ in range(len(fix_branch_1_commits)) + ], }, }, ], @@ -337,6 +390,10 @@ def _get_repo_from_definition( ) feat_branch_pr_number = next(pr_num_gen) + cid_feat_branch_squash_base = "feat_branch_1_squash" + cid_feat_branch_squash_gen = ( + f"{cid_feat_branch_squash_base}-{i}" for i in count(start=1) + ) feat_branch_squash_commit_spec: CommitSpec = { **{ # type: ignore[typeddict-item] cmt_type: format_squash_commit_msg_github( @@ -349,11 +406,12 @@ def _get_repo_from_definition( ) for cmt_type in all_commit_types }, + "cid": cid_feat_branch_squash_base, "datetime": next(commit_timestamp_gen), "include_in_changelog": True, } - new_version = "1.1.0" + new_version = Version.parse("1.1.0", tag_format=new_version.tag_format) repo_construction_steps.extend( [ @@ -365,13 +423,15 @@ def _get_repo_from_definition( "commit_def": convert_commit_spec_to_commit_def( feat_branch_squash_commit_spec, commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -379,6 +439,10 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + next(cid_feat_branch_squash_gen) + for _ in range(len(feat_branch_1_commits) + 1) + ], }, }, ], diff --git a/tests/fixtures/repos/github_flow/repo_w_default_release_w_branch_update_merge.py b/tests/fixtures/repos/github_flow/repo_w_default_release_w_branch_update_merge.py index 1d6e8bf95..b6ca93954 100644 --- a/tests/fixtures/repos/github_flow/repo_w_default_release_w_branch_update_merge.py +++ b/tests/fixtures/repos/github_flow/repo_w_default_release_w_branch_update_merge.py @@ -3,11 +3,12 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.version.version import Version import tests.conftest import tests.const @@ -20,7 +21,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -39,15 +48,19 @@ ConvertCommitSpecsToCommitDefsFn, ConvertCommitSpecToCommitDefFn, ExProjectGitRepoFn, + FormatGitHubMergeCommitMsgFn, FormatGitMergeCommitMsgFn, GetRepoDefinitionFn, + RepoActionGitMerge, + RepoActionGitMergeDetails, RepoActions, RepoActionWriteChangelogsDestFile, TomlSerializableTypes, ) -FEAT_BRANCH_NAME = "feat/feature" +FEAT_BRANCH_1_NAME = "feat/feature-1" +FEAT_BRANCH_2_NAME = "feat/feature-2" @pytest.fixture(scope="session") @@ -82,9 +95,14 @@ def get_repo_definition_4_github_flow_repo_w_default_release_n_branch_update_mer convert_commit_specs_to_commit_defs: ConvertCommitSpecsToCommitDefsFn, convert_commit_spec_to_commit_def: ConvertCommitSpecToCommitDefFn, format_merge_commit_msg_git: FormatGitMergeCommitMsgFn, + format_merge_commit_msg_github: FormatGitHubMergeCommitMsgFn, changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, + default_tag_format_str: str, ) -> GetRepoDefinitionFn: """ This fixture provides a function that builds a repository definition for a trunk-based development @@ -95,6 +113,11 @@ def get_repo_definition_4_github_flow_repo_w_default_release_n_branch_update_mer It is the minimal reproducible example of the issue [#1252](https://github.com/python-semantic-release/python-semantic-release/issues/1252). """ + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } def _get_repo_from_definition( commit_type: CommitConvention, @@ -106,19 +129,26 @@ def _get_repo_from_definition( ignore_merge_commits: bool = True, ) -> Sequence[RepoActions]: stable_now_datetime = stable_now_date() + pr_num_gen = (i for i in count(start=2, step=1)) commit_timestamp_gen = ( (stable_now_datetime + timedelta(seconds=i)).isoformat(timespec="seconds") for i in count(step=1) ) + commit_parser = cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ) changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ] @@ -131,7 +161,7 @@ def _get_repo_from_definition( "commit_type": commit_type, "hvcs_client_name": hvcs_client_name, "hvcs_domain": hvcs_domain, - "tag_format_str": tag_format_str, + "tag_format_str": tag_format_str or default_tag_format_str, "mask_initial_release": mask_initial_release, "extra_configs": { # Set the default release branch @@ -139,7 +169,8 @@ def _get_repo_from_definition( "match": r"^(main|master)$", "prerelease": False, }, - "tool.semantic_release.allow_zero_version": True, + "tool.semantic_release.allow_zero_version": False, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, **(extra_configs or {}), }, }, @@ -147,7 +178,9 @@ def _get_repo_from_definition( ) # Make initial release - new_version = "0.1.0" + new_version = Version.parse( + "1.0.0", tag_format=(tag_format_str or default_tag_format_str) + ) repo_construction_steps.extend( [ @@ -157,6 +190,9 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_db_c1_initial := "db_c1_initial_commit" + ), "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -166,6 +202,7 @@ def _get_repo_from_definition( ), }, { + "cid": (cid_db_c2_feat := "db_c2_feat"), "conventional": "feat: add new feature", "emoji": ":sparkles: add new feature", "scipy": "ENH: add new feature", @@ -174,13 +211,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -188,6 +227,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_db_c1_initial, cid_db_c2_feat], }, }, ], @@ -196,72 +236,53 @@ def _get_repo_from_definition( ] ) - # Create a feature branch (without commits yet, just to pin a commit) + # Create a feature branch & make a commit (separate developer, slower activity) repo_construction_steps.extend( [ { "action": RepoActionStep.GIT_CHECKOUT, "details": { "create_branch": { - "name": FEAT_BRANCH_NAME, + "name": FEAT_BRANCH_1_NAME, "start_branch": DEFAULT_BRANCH_NAME, } }, }, - { - "action": RepoActionStep.GIT_CHECKOUT, - "details": {"branch": DEFAULT_BRANCH_NAME}, - }, - ] - ) - - # Make another release in default branch - new_version = "0.2.0" - - repo_construction_steps.extend( - [ { "action": RepoActionStep.MAKE_COMMITS, "details": { "commits": convert_commit_specs_to_commit_defs( [ { - "conventional": "feat: add another feature", - "emoji": ":sparkles: add another feature", - "scipy": "ENH: add another feature", + "cid": ( + cid_feb1_c1_feat := "feat_branch_1_c1_feat" + ), + "conventional": "feat: add new feature in the feature branch", + "emoji": ":sparkles: add new feature in the feature branch", + "scipy": "ENH: add new feature in the feature branch", "datetime": next(commit_timestamp_gen), "include_in_changelog": True, }, ], commit_type, + parser=commit_parser, ), }, }, - { - "action": RepoActionStep.RELEASE, - "details": { - "version": new_version, - "datetime": next(commit_timestamp_gen), - "pre_actions": [ - { - "action": RepoActionStep.WRITE_CHANGELOGS, - "details": { - "new_version": new_version, - "dest_files": changelog_file_definitions, - }, - }, - ], - }, - }, ] ) - # Add commit to the feature branch + # Create a 2nd feature branch & make a commit (separate developer, faster activity) repo_construction_steps.extend( [ { "action": RepoActionStep.GIT_CHECKOUT, - "details": {"branch": FEAT_BRANCH_NAME}, + "details": { + "create_branch": { + "name": FEAT_BRANCH_2_NAME, + "start_branch": DEFAULT_BRANCH_NAME, + } + }, }, { "action": RepoActionStep.MAKE_COMMITS, @@ -269,91 +290,156 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { - "conventional": "feat: add new feature in the feature branch", - "emoji": ":sparkles: add new feature in the feature branch", - "scipy": "ENH: add new feature in the feature branch", + "cid": ( + cid_feb2_c1_feat := "feat_branch_2_c1_feat" + ), + "conventional": "feat: add a faster feature", + "emoji": ":sparkles: add a faster feature", + "scipy": "ENH: add a faster feature", "datetime": next(commit_timestamp_gen), "include_in_changelog": True, }, ], commit_type, + parser=commit_parser, ), }, }, ] ) - # Merge default branch into the feature branch to keep it up to date + # Merge feature branch 2 into default branch and release (faster activity is complete) + new_version = Version.parse("1.1.0", tag_format=new_version.tag_format) + + merge_def_type_placeholder: RepoActionGitMerge[RepoActionGitMergeDetails] = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_2_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb2_merge := "feat_branch_2_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_github( + pr_number=next(pr_num_gen), + branch_name=FEAT_BRANCH_2_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + repo_construction_steps.extend( [ { - "action": RepoActionStep.GIT_MERGE, + "action": RepoActionStep.GIT_CHECKOUT, + "details": {"branch": DEFAULT_BRANCH_NAME}, + }, + merge_def_type_placeholder, + { + "action": RepoActionStep.RELEASE, "details": { - "branch_name": DEFAULT_BRANCH_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( + "version": str(new_version), + "tag_format": new_version.tag_format, + "datetime": next(commit_timestamp_gen), + "pre_actions": [ { - "conventional": format_merge_commit_msg_git( - branch_name=DEFAULT_BRANCH_NAME, - tgt_branch_name=FEAT_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=DEFAULT_BRANCH_NAME, - tgt_branch_name=FEAT_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=DEFAULT_BRANCH_NAME, - tgt_branch_name=FEAT_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": new_version, + "dest_files": changelog_file_definitions, + "commit_ids": [cid_feb2_c1_feat, cid_feb2_merge], + }, }, - commit_type, - ), + ], }, }, + ] + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": DEFAULT_BRANCH_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb1_update_merge := "feat_branch_1_update_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_git( + branch_name=DEFAULT_BRANCH_NAME, + tgt_branch_name=FEAT_BRANCH_1_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + + # Merge default branch into the feature branch to keep it up to date + repo_construction_steps.extend( + [ { "action": RepoActionStep.GIT_CHECKOUT, - "details": {"branch": DEFAULT_BRANCH_NAME}, + "details": {"branch": FEAT_BRANCH_1_NAME}, }, + merge_def_type_placeholder, ] ) # Merge the feature branch into the default branch and make a release - new_version = "0.3.0" + new_version = Version.parse("1.2.0", tag_format=new_version.tag_format) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_1_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb1_merge := "feat_branch_1_release_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_github( + pr_number=next(pr_num_gen), + branch_name=FEAT_BRANCH_1_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } repo_construction_steps.extend( [ { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "emoji": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "scipy": format_merge_commit_msg_git( - branch_name=FEAT_BRANCH_NAME, - tgt_branch_name=DEFAULT_BRANCH_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, + "action": RepoActionStep.GIT_CHECKOUT, + "details": {"branch": DEFAULT_BRANCH_NAME}, }, + merge_def_type_placeholder, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -361,6 +447,11 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_feb1_c1_feat, + cid_feb1_update_merge, + cid_feb1_merge, + ], }, }, ], diff --git a/tests/fixtures/repos/github_flow/repo_w_release_channels.py b/tests/fixtures/repos/github_flow/repo_w_release_channels.py index 87c57b609..4e3fb87be 100644 --- a/tests/fixtures/repos/github_flow/repo_w_release_channels.py +++ b/tests/fixtures/repos/github_flow/repo_w_release_channels.py @@ -3,11 +3,12 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.version.version import Version import tests.conftest import tests.const @@ -20,7 +21,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -39,6 +48,8 @@ ExProjectGitRepoFn, FormatGitHubMergeCommitMsgFn, GetRepoDefinitionFn, + RepoActionGitMerge, + RepoActionGitMergeDetails, RepoActions, RepoActionWriteChangelogsDestFile, TomlSerializableTypes, @@ -85,13 +96,22 @@ def get_repo_definition_4_github_flow_repo_w_feature_release_channel( changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, + default_tag_format_str: str, ) -> GetRepoDefinitionFn: """ Builds a repository with the GitHub Flow branching strategy using merge commits for alpha feature releases and official releases on the default branch. """ + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } - def _get_repo_from_defintion( + def _get_repo_from_definition( commit_type: CommitConvention, hvcs_client_name: str = "github", hvcs_domain: str = EXAMPLE_HVCS_DOMAIN, @@ -106,15 +126,21 @@ def _get_repo_from_defintion( for i in count(step=1) ) pr_num_gen = (i for i in count(start=2, step=1)) + commit_parser = cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ) changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ] @@ -127,7 +153,7 @@ def _get_repo_from_defintion( "commit_type": commit_type, "hvcs_client_name": hvcs_client_name, "hvcs_domain": hvcs_domain, - "tag_format_str": tag_format_str, + "tag_format_str": tag_format_str or default_tag_format_str, "mask_initial_release": mask_initial_release, "extra_configs": { # Set the default release branch @@ -141,6 +167,7 @@ def _get_repo_from_defintion( "prerelease": True, "prerelease_token": "alpha", }, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, "tool.semantic_release.allow_zero_version": False, **(extra_configs or {}), }, @@ -149,7 +176,9 @@ def _get_repo_from_defintion( ) # Make initial release - new_version = "1.0.0" + new_version = Version.parse( + "1.0.0", tag_format=(tag_format_str or default_tag_format_str) + ) repo_construction_steps.extend( [ @@ -159,6 +188,7 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_db_initial := "db_c1_initial_commit"), "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -168,6 +198,7 @@ def _get_repo_from_defintion( ), }, { + "cid": (cid_db_c2_feat := "db_c2_feat"), "conventional": "feat: add new feature", "emoji": ":sparkles: add new feature", "scipy": "ENH: add new feature", @@ -176,13 +207,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -190,6 +223,7 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_db_initial, cid_db_c2_feat], }, }, ], @@ -199,7 +233,7 @@ def _get_repo_from_defintion( ) # Make a fix and release it as an alpha release - new_version = "1.0.1-alpha.1" + new_version = Version.parse("1.0.1-alpha.1", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -217,6 +251,7 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_fib1_c1_fix := "fix_branch_c1_fix"), "conventional": "fix: correct some text\n\nResolves: #123", "emoji": ":bug: correct some text\n\nResolves: #123", "scipy": "MAINT: correct some text\n\nResolves: #123", @@ -225,13 +260,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -239,6 +276,7 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_fib1_c1_fix], }, }, ], @@ -248,7 +286,7 @@ def _get_repo_from_defintion( ) # Update the fix and release another alpha release - new_version = "1.0.1-alpha.2" + new_version = Version.parse("1.0.1-alpha.2", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -257,6 +295,7 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_fib1_c2_fix := "fix_branch_1_c2_fix"), "conventional": "fix: adjust text to resolve", "emoji": ":bug: adjust text to resolve", "scipy": "MAINT: adjust text to resolve", @@ -265,13 +304,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -279,6 +320,7 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_fib1_c2_fix], }, }, ], @@ -288,44 +330,45 @@ def _get_repo_from_defintion( ) # Merge the fix branch into the default branch and formally release it - new_version = "1.0.1" - fix_branch_pr_number = next(pr_num_gen) + new_version = Version.parse("1.0.1", tag_format=new_version.tag_format) + + merge_def_type_placeholder: RepoActionGitMerge[RepoActionGitMergeDetails] = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FIX_BRANCH_1_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_fib1_merge := "fix_branch_1_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_github( + pr_number=next(pr_num_gen), + branch_name=FIX_BRANCH_1_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + repo_construction_steps.extend( [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FIX_BRANCH_1_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_github( - pr_number=fix_branch_pr_number, - branch_name=FIX_BRANCH_1_NAME, - ), - "emoji": format_merge_commit_msg_github( - pr_number=fix_branch_pr_number, - branch_name=FIX_BRANCH_1_NAME, - ), - "scipy": format_merge_commit_msg_github( - pr_number=fix_branch_pr_number, - branch_name=FIX_BRANCH_1_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -333,6 +376,7 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_fib1_merge], }, }, ], @@ -342,7 +386,7 @@ def _get_repo_from_defintion( ) # Make a feature branch and release it as an alpha release - new_version = "1.1.0-alpha.1" + new_version = Version.parse("1.1.0-alpha.1", tag_format=new_version.tag_format) repo_construction_steps.extend( [ @@ -361,6 +405,9 @@ def _get_repo_from_defintion( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": ( + cid_feb1_c1_feat := "feat_branch_1_c1_feat" + ), "conventional": "feat(cli): add cli interface", "emoji": ":sparkles: add cli interface", "scipy": "ENH: add cli interface", @@ -369,13 +416,15 @@ def _get_repo_from_defintion( }, ], commit_type, + parser=commit_parser, ) }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -383,6 +432,7 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_feb1_c1_feat], }, }, ], @@ -392,44 +442,45 @@ def _get_repo_from_defintion( ) # Merge the feature branch and officially release it - new_version = "1.1.0" - feat_branch_pr_number = next(pr_num_gen) + new_version = Version.parse("1.1.0", tag_format=new_version.tag_format) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_1_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_feb1_merge := "feat_branch_1_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_github( + pr_number=next(pr_num_gen), + branch_name=FEAT_BRANCH_1_NAME, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=commit_parser, + ), + }, + } + repo_construction_steps.extend( [ { "action": RepoActionStep.GIT_CHECKOUT, "details": {"branch": DEFAULT_BRANCH_NAME}, }, - { - "action": RepoActionStep.GIT_MERGE, - "details": { - "branch_name": FEAT_BRANCH_1_NAME, - "fast_forward": False, - "commit_def": convert_commit_spec_to_commit_def( - { - "conventional": format_merge_commit_msg_github( - pr_number=feat_branch_pr_number, - branch_name=FEAT_BRANCH_1_NAME, - ), - "emoji": format_merge_commit_msg_github( - pr_number=feat_branch_pr_number, - branch_name=FEAT_BRANCH_1_NAME, - ), - "scipy": format_merge_commit_msg_github( - pr_number=feat_branch_pr_number, - branch_name=FEAT_BRANCH_1_NAME, - ), - "datetime": next(commit_timestamp_gen), - "include_in_changelog": not ignore_merge_commits, - }, - commit_type, - ), - }, - }, + merge_def_type_placeholder, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -437,6 +488,7 @@ def _get_repo_from_defintion( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_feb1_merge], }, }, ], @@ -447,7 +499,7 @@ def _get_repo_from_defintion( return repo_construction_steps - return _get_repo_from_defintion + return _get_repo_from_definition @pytest.fixture(scope="session") diff --git a/tests/fixtures/repos/repo_initial_commit.py b/tests/fixtures/repos/repo_initial_commit.py index 7bc1fc3bb..cef6eacfe 100644 --- a/tests/fixtures/repos/repo_initial_commit.py +++ b/tests/fixtures/repos/repo_initial_commit.py @@ -1,7 +1,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest @@ -17,7 +17,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -70,7 +78,16 @@ def get_repo_definition_4_repo_w_initial_commit( changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, ) -> GetRepoDefinitionFn: + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } + def _get_repo_from_definition( commit_type: CommitConvention, hvcs_client_name: str = "github", @@ -97,6 +114,7 @@ def _get_repo_from_definition( "match": r"^(main|master)$", "prerelease": False, }, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, **(extra_configs or {}), }, }, @@ -107,6 +125,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": "initial_commit", "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -119,6 +138,10 @@ def _get_repo_from_definition( }, ], commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ), ), }, }, @@ -130,12 +153,17 @@ def _get_repo_from_definition( { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ], + "commit_ids": [ + "initial_commit", + ], }, }, ] diff --git a/tests/fixtures/repos/trunk_based_dev/repo_w_dual_version_support.py b/tests/fixtures/repos/trunk_based_dev/repo_w_dual_version_support.py index 008a7b6d6..a0b6f8b16 100644 --- a/tests/fixtures/repos/trunk_based_dev/repo_w_dual_version_support.py +++ b/tests/fixtures/repos/trunk_based_dev/repo_w_dual_version_support.py @@ -3,11 +3,12 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.version.version import Version import tests.conftest import tests.const @@ -20,7 +21,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -79,11 +88,20 @@ def get_repo_definition_4_trunk_only_repo_w_dual_version_support( changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, + default_tag_format_str: str, ) -> GetRepoDefinitionFn: """ Builds a repository with trunk-only committing (no-branching) strategy with only official releases with latest and previous version support. """ + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } def _get_repo_from_definition( commit_type: CommitConvention, @@ -99,15 +117,21 @@ def _get_repo_from_definition( (stable_now_datetime + timedelta(seconds=i)).isoformat(timespec="seconds") for i in count(step=1) ) + commit_parser = cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ) changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ] @@ -120,7 +144,7 @@ def _get_repo_from_definition( "commit_type": commit_type, "hvcs_client_name": hvcs_client_name, "hvcs_domain": hvcs_domain, - "tag_format_str": tag_format_str, + "tag_format_str": tag_format_str or default_tag_format_str, "mask_initial_release": mask_initial_release, "extra_configs": { # Set the default release branch @@ -132,6 +156,7 @@ def _get_repo_from_definition( "match": r"^v1\.x$", "prerelease": False, }, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, "tool.semantic_release.allow_zero_version": False, **(extra_configs or {}), }, @@ -140,7 +165,9 @@ def _get_repo_from_definition( ) # Make initial release - new_version = "1.0.0" + new_version = Version.parse( + "1.0.0", tag_format=tag_format_str or default_tag_format_str + ) repo_construction_steps.extend( [ @@ -150,6 +177,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c1_initial := "c1_initial_commit"), "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -159,6 +187,7 @@ def _get_repo_from_definition( ), }, { + "cid": (cid_c2_feat := "c2-feat"), "conventional": "feat: add new feature", "emoji": ":sparkles: add new feature", "scipy": "ENH: add new feature", @@ -167,13 +196,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -181,6 +212,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c1_initial, cid_c2_feat], }, }, ], @@ -190,7 +222,7 @@ def _get_repo_from_definition( ) # Make a fix and officially release it - new_version = "1.0.1" + new_version = Version.parse("1.0.1", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -199,6 +231,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c3_fix := "c3-fix"), "conventional": "fix: correct some text", "emoji": ":bug: correct some text", "scipy": "MAINT: correct some text", @@ -207,13 +240,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -221,6 +256,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c3_fix], }, }, ], @@ -230,7 +266,7 @@ def _get_repo_from_definition( ) # Make a breaking change and officially release it - new_version = "2.0.0" + new_version = Version.parse("2.0.0", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -252,6 +288,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c4_break_feat := "c4-break-feat"), "conventional": str.join( "\n\n", [ @@ -278,13 +315,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -292,6 +331,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c4_break_feat], }, }, ], @@ -301,7 +341,7 @@ def _get_repo_from_definition( ) # Fix a critical bug in the previous version and officially release it - new_version = "1.0.2" + new_version = Version.parse("1.0.2", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -314,6 +354,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c5_v1_fix := "c5-fix"), "conventional": "fix: correct critical bug\n\nResolves: #123\n", "emoji": ":bug: correct critical bug\n\nResolves: #123\n", "scipy": "MAINT: correct critical bug\n\nResolves: #123\n", @@ -322,13 +363,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -337,6 +380,7 @@ def _get_repo_from_definition( "new_version": new_version, "max_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c5_v1_fix], }, }, ], diff --git a/tests/fixtures/repos/trunk_based_dev/repo_w_dual_version_support_w_prereleases.py b/tests/fixtures/repos/trunk_based_dev/repo_w_dual_version_support_w_prereleases.py index 4fb0d14c7..557f9f38b 100644 --- a/tests/fixtures/repos/trunk_based_dev/repo_w_dual_version_support_w_prereleases.py +++ b/tests/fixtures/repos/trunk_based_dev/repo_w_dual_version_support_w_prereleases.py @@ -3,11 +3,12 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.version.version import Version import tests.conftest import tests.const @@ -20,7 +21,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -79,11 +88,20 @@ def get_repo_definition_4_trunk_only_repo_w_dual_version_spt_w_prereleases( changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, + default_tag_format_str: str, ) -> GetRepoDefinitionFn: """ Builds a repository with trunk-only committing (no-branching) strategy with only official releases with latest and previous version support. """ + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } def _get_repo_from_definition( commit_type: CommitConvention, @@ -99,15 +117,21 @@ def _get_repo_from_definition( (stable_now_datetime + timedelta(seconds=i)).isoformat(timespec="seconds") for i in count(step=1) ) + commit_parser = cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ) changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ] @@ -120,7 +144,7 @@ def _get_repo_from_definition( "commit_type": commit_type, "hvcs_client_name": hvcs_client_name, "hvcs_domain": hvcs_domain, - "tag_format_str": tag_format_str, + "tag_format_str": tag_format_str or default_tag_format_str, "mask_initial_release": mask_initial_release, "extra_configs": { # Set the default release branch @@ -132,6 +156,7 @@ def _get_repo_from_definition( "match": r"^v1\.x$", "prerelease": False, }, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, "tool.semantic_release.allow_zero_version": False, **(extra_configs or {}), }, @@ -140,7 +165,9 @@ def _get_repo_from_definition( ) # Make initial release - new_version = "1.0.0" + new_version = Version.parse( + "1.0.0", tag_format=(tag_format_str or default_tag_format_str) + ) repo_construction_steps.extend( [ @@ -150,6 +177,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c1_initial := "c1-initial_commit"), "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -159,6 +187,7 @@ def _get_repo_from_definition( ), }, { + "cid": (cid_c2_feat := "c2-feat"), "conventional": "feat: add new feature", "emoji": ":sparkles: add new feature", "scipy": "ENH: add new feature", @@ -167,13 +196,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -181,6 +212,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c1_initial, cid_c2_feat], }, }, ], @@ -190,7 +222,7 @@ def _get_repo_from_definition( ) # Make a fix and officially release it - new_version = "1.0.1" + new_version = Version.parse("1.0.1", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -199,6 +231,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c3_fix := "c3-fix"), "conventional": "fix: correct some text", "emoji": ":bug: correct some text", "scipy": "MAINT: correct some text", @@ -207,13 +240,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -221,6 +256,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c3_fix], }, }, ], @@ -230,7 +266,7 @@ def _get_repo_from_definition( ) # Make a breaking change and officially release it - new_version = "2.0.0" + new_version = Version.parse("2.0.0", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -252,6 +288,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c4_bfeat := "c4-breaking-feat1"), "conventional": str.join( "\n\n", [ @@ -278,13 +315,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -292,6 +331,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c4_bfeat], }, }, ], @@ -302,7 +342,7 @@ def _get_repo_from_definition( # Attempt to fix a critical bug in the previous version and release it as a prerelease version # This is based on https://github.com/python-semantic-release/python-semantic-release/issues/861 - new_version = "1.0.2-hotfix.1" + new_version = Version.parse("1.0.2-hotfix.1", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -315,6 +355,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c5_v1_fix := "c5-fix2"), "conventional": "fix: correct critical bug\n\nResolves: #123\n", "emoji": ":bug: correct critical bug\n\nResolves: #123\n", "scipy": "MAINT: correct critical bug\n\nResolves: #123\n", @@ -323,13 +364,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -338,6 +381,7 @@ def _get_repo_from_definition( "new_version": new_version, "max_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c5_v1_fix], }, }, ], @@ -347,7 +391,7 @@ def _get_repo_from_definition( ) # The Hotfix didn't work, so correct it and try again - new_version = "1.0.2-hotfix.2" + new_version = Version.parse("1.0.2-hotfix.2", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -356,6 +400,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c6_v1_fix := "c6-fix3"), "conventional": "fix: resolve critical bug", "emoji": ":bug: resolve critical bug", "scipy": "MAINT: resolve critical bug", @@ -364,13 +409,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -379,6 +426,7 @@ def _get_repo_from_definition( "new_version": new_version, "max_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c6_v1_fix], }, }, ], @@ -388,7 +436,7 @@ def _get_repo_from_definition( ) # It finally was resolved so release it officially - new_version = "1.0.2" + new_version = Version.parse("1.0.2", tag_format=new_version.tag_format) repo_construction_steps.extend( [ # { @@ -397,6 +445,7 @@ def _get_repo_from_definition( # "commits": convert_commit_specs_to_commit_defs( # [ # { + # "cid": (cid_c7_v1_docs := "c7-docs"), # "conventional": "docs: update documentation regarding critical bug", # "emoji": ":books: update documentation regarding critical bug", # "scipy": "DOC: update documentation regarding critical bug", @@ -405,13 +454,15 @@ def _get_repo_from_definition( # }, # ], # commit_type, + # parser=commit_parser, # ), # }, # }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -420,6 +471,11 @@ def _get_repo_from_definition( "new_version": new_version, "max_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_c5_v1_fix, + cid_c6_v1_fix, + # cid_c7_v1_docs, + ], }, }, ], diff --git a/tests/fixtures/repos/trunk_based_dev/repo_w_no_tags.py b/tests/fixtures/repos/trunk_based_dev/repo_w_no_tags.py index a1253c8e8..dc2362f39 100644 --- a/tests/fixtures/repos/trunk_based_dev/repo_w_no_tags.py +++ b/tests/fixtures/repos/trunk_based_dev/repo_w_no_tags.py @@ -3,7 +3,7 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest @@ -19,7 +19,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -72,11 +80,19 @@ def get_repo_definition_4_trunk_only_repo_w_no_tags( changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, ) -> GetRepoDefinitionFn: """ Builds a repository with trunk-only committing (no-branching) strategy without any releases. """ + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } def _get_repo_from_definition( commit_type: CommitConvention, @@ -110,6 +126,7 @@ def _get_repo_from_definition( "match": r"^(main|master)$", "prerelease": False, }, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, **(extra_configs or {}), }, }, @@ -120,6 +137,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c1_initial := "c1_initial_commit"), "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -129,6 +147,7 @@ def _get_repo_from_definition( ), }, { + "cid": (cid_c2_feat1 := "c2-feat1"), "conventional": "feat: add new feature", "emoji": ":sparkles: add new feature", "scipy": "ENH: add new feature", @@ -136,6 +155,7 @@ def _get_repo_from_definition( "include_in_changelog": True, }, { + "cid": (cid_c3_fix1 := "c3-fix1"), "conventional": "fix: correct some text", "emoji": ":bug: correct some text", "scipy": "MAINT: correct some text", @@ -143,6 +163,7 @@ def _get_repo_from_definition( "include_in_changelog": True, }, { + "cid": (cid_c4_fix2 := "c4-fix2"), "conventional": "fix: correct more text\n\nCloses: #123", "emoji": ":bug: correct more text\n\nCloses: #123", "scipy": "MAINT: correct more text\n\nCloses: #123", @@ -151,6 +172,10 @@ def _get_repo_from_definition( }, ], commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ), ), }, }, @@ -162,12 +187,20 @@ def _get_repo_from_definition( { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ], + "commit_ids": [ + cid_c1_initial, + cid_c2_feat1, + cid_c3_fix1, + cid_c4_fix2, + ], }, }, ] @@ -312,7 +345,7 @@ def repo_w_no_tags_conventional_commits_unmasked_initial_release( example_project_dir: ExProjectDir, change_to_ex_proj_dir: None, ) -> BuiltRepoResult: - """Replicates repo with no tags, but with allow_zero_version=True""" + """Replicates repo with no tags, but with mask_initial_release=False""" repo_name = repo_w_no_tags_conventional_commits_unmasked_initial_release.__name__ commit_type: CommitConvention = ( repo_name.split("_commits", maxsplit=1)[0].split("_")[-1] # type: ignore[assignment] diff --git a/tests/fixtures/repos/trunk_based_dev/repo_w_prereleases.py b/tests/fixtures/repos/trunk_based_dev/repo_w_prereleases.py index 57e46578f..f9536183d 100644 --- a/tests/fixtures/repos/trunk_based_dev/repo_w_prereleases.py +++ b/tests/fixtures/repos/trunk_based_dev/repo_w_prereleases.py @@ -3,11 +3,12 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.version.version import Version import tests.conftest import tests.const @@ -19,7 +20,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -73,11 +82,20 @@ def get_repo_definition_4_trunk_only_repo_w_prerelease_tags( changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, + default_tag_format_str: str, ) -> GetRepoDefinitionFn: """ Builds a repository with trunk-only committing (no-branching) strategy with official and prereleases releases. """ + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } def _get_repo_from_definition( commit_type: CommitConvention, @@ -93,15 +111,21 @@ def _get_repo_from_definition( (stable_now_datetime + timedelta(seconds=i)).isoformat(timespec="seconds") for i in count(step=1) ) + commit_parser = cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ) changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ] @@ -114,7 +138,7 @@ def _get_repo_from_definition( "commit_type": commit_type, "hvcs_client_name": hvcs_client_name, "hvcs_domain": hvcs_domain, - "tag_format_str": tag_format_str, + "tag_format_str": tag_format_str or default_tag_format_str, "mask_initial_release": mask_initial_release, "extra_configs": { # Set the default release branch @@ -122,6 +146,7 @@ def _get_repo_from_definition( "match": r"^(main|master)$", "prerelease": False, }, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, "tool.semantic_release.allow_zero_version": True, **(extra_configs or {}), }, @@ -130,7 +155,9 @@ def _get_repo_from_definition( ) # Make initial release - new_version = "0.1.0" + new_version = Version.parse( + "0.1.0", tag_format=(tag_format_str or default_tag_format_str) + ) repo_construction_steps.extend( [ @@ -140,6 +167,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c1_initial := "c1-initial_commit"), "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -149,6 +177,7 @@ def _get_repo_from_definition( ), }, { + "cid": (cid_c2_feat := "c2-feat"), "conventional": "feat: add new feature", "emoji": ":sparkles: add new feature", "scipy": "ENH: add new feature", @@ -157,13 +186,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -171,6 +202,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c1_initial, cid_c2_feat], }, }, ], @@ -180,7 +212,7 @@ def _get_repo_from_definition( ) # Make a fix and release it as a release candidate - new_version = "0.1.1-rc.1" + new_version = Version.parse("0.1.1-rc.1", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -189,6 +221,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c3_fix := "c2-fix"), "conventional": "fix: correct some text\n\nfixes: #123\n", "emoji": ":bug: correct some text\n\nfixes: #123\n", "scipy": "MAINT: correct some text\n\nfixes: #123\n", @@ -197,13 +230,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -211,6 +246,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c3_fix], }, }, ], @@ -220,7 +256,7 @@ def _get_repo_from_definition( ) # Make an additional feature change and release it as a new release candidate - new_version = "0.2.0-rc.1" + new_version = Version.parse("0.2.0-rc.1", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -229,6 +265,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c4_feat := "c4-feat"), "conventional": "feat: add some more text", "emoji": ":sparkles: add some more text", "scipy": "ENH: add some more text", @@ -237,13 +274,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -251,6 +290,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c4_feat], }, }, ], @@ -260,7 +300,7 @@ def _get_repo_from_definition( ) # Make an additional feature change and officially release the latest - new_version = "0.2.0" + new_version = Version.parse("0.2.0", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -269,6 +309,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c5_feat := "c5-feat"), "conventional": "feat(cli): add cli command", "emoji": ":sparkles:(cli) add cli command", "scipy": "ENH: cli: add cli command", @@ -277,13 +318,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -291,6 +334,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c5_feat], }, }, ], diff --git a/tests/fixtures/repos/trunk_based_dev/repo_w_tags.py b/tests/fixtures/repos/trunk_based_dev/repo_w_tags.py index b58a23865..6121b3699 100644 --- a/tests/fixtures/repos/trunk_based_dev/repo_w_tags.py +++ b/tests/fixtures/repos/trunk_based_dev/repo_w_tags.py @@ -3,11 +3,12 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.version.version import Version import tests.conftest import tests.const @@ -19,7 +20,15 @@ ) if TYPE_CHECKING: - from typing import Sequence + from typing import Any, Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from semantic_release.commit_parser.emoji import EmojiCommitParser + from semantic_release.commit_parser.scipy import ScipyCommitParser + from semantic_release.commit_parser.token import ParseResult from tests.conftest import ( GetCachedRepoDataFn, @@ -75,11 +84,20 @@ def get_repo_definition_4_trunk_only_repo_w_tags( changelog_md_file: Path, changelog_rst_file: Path, stable_now_date: GetStableDateNowFn, + default_conventional_parser: ConventionalCommitParser, + default_emoji_parser: EmojiCommitParser, + default_scipy_parser: ScipyCommitParser, + default_tag_format_str: str, ) -> GetRepoDefinitionFn: """ Builds a repository with trunk-only committing (no-branching) strategy with only official releases. """ + parser_classes: dict[CommitConvention, CommitParser[Any, Any]] = { + "conventional": default_conventional_parser, + "emoji": default_emoji_parser, + "scipy": default_scipy_parser, + } def _get_repo_from_definition( commit_type: CommitConvention, @@ -95,15 +113,21 @@ def _get_repo_from_definition( (stable_now_datetime + timedelta(seconds=i)).isoformat(timespec="seconds") for i in count(step=1) ) + commit_parser = cast( + "CommitParser[ParseResult, ParserOptions]", + parser_classes[commit_type], + ) changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ { "path": changelog_md_file, "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": mask_initial_release, }, { "path": changelog_rst_file, "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": mask_initial_release, }, ] @@ -116,7 +140,7 @@ def _get_repo_from_definition( "commit_type": commit_type, "hvcs_client_name": hvcs_client_name, "hvcs_domain": hvcs_domain, - "tag_format_str": tag_format_str, + "tag_format_str": tag_format_str or default_tag_format_str, "mask_initial_release": mask_initial_release, "extra_configs": { # Set the default release branch @@ -124,6 +148,7 @@ def _get_repo_from_definition( "match": r"^(main|master)$", "prerelease": False, }, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": ignore_merge_commits, "tool.semantic_release.allow_zero_version": True, **(extra_configs or {}), }, @@ -132,7 +157,9 @@ def _get_repo_from_definition( ) # Make initial release - new_version = "0.1.0" + new_version = Version.parse( + "0.1.0", tag_format=(tag_format_str or default_tag_format_str) + ) repo_construction_steps.extend( [ @@ -142,6 +169,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c1_initial := "c1_initial_commit"), "conventional": INITIAL_COMMIT_MESSAGE, "emoji": INITIAL_COMMIT_MESSAGE, "scipy": INITIAL_COMMIT_MESSAGE, @@ -151,6 +179,7 @@ def _get_repo_from_definition( ), }, { + "cid": (cid_c2_feat := "c2-feat"), "conventional": "feat: add new feature", "emoji": ":sparkles: add new feature", "scipy": "ENH: add new feature", @@ -159,13 +188,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -173,6 +204,10 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [ + cid_c1_initial, + cid_c2_feat, + ], }, }, ], @@ -182,7 +217,7 @@ def _get_repo_from_definition( ) # Make a fix and officially release it - new_version = "0.1.1" + new_version = Version.parse("0.1.1", tag_format=new_version.tag_format) repo_construction_steps.extend( [ { @@ -191,6 +226,7 @@ def _get_repo_from_definition( "commits": convert_commit_specs_to_commit_defs( [ { + "cid": (cid_c3_fix := "c3-fix"), "conventional": "fix: correct some text\n\nResolves: #123\n", "emoji": ":bug: correct some text\n\nResolves: #123\n", "scipy": "MAINT: correct some text\n\nResolves: #123\n", @@ -199,13 +235,15 @@ def _get_repo_from_definition( }, ], commit_type, + parser=commit_parser, ), }, }, { "action": RepoActionStep.RELEASE, "details": { - "version": new_version, + "version": str(new_version), + "tag_format": new_version.tag_format, "datetime": next(commit_timestamp_gen), "pre_actions": [ { @@ -213,6 +251,7 @@ def _get_repo_from_definition( "details": { "new_version": new_version, "dest_files": changelog_file_definitions, + "commit_ids": [cid_c3_fix], }, }, ], From aacc117c601e73f8dc70800ed167f64c2ceff7cd Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 1 Sep 2025 20:46:27 -0600 Subject: [PATCH 4/7] test(cmd-version): refactor version command tests to match new e2e infrastructure --- .../e2e/cmd_version/bump_version/conftest.py | 4 +- .../git_flow/test_repo_1_channel.py | 48 +++++++------ .../git_flow/test_repo_2_channels.py | 48 +++++++------ .../git_flow/test_repo_3_channels.py | 48 +++++++------ .../git_flow/test_repo_4_channels.py | 48 +++++++------ .../github_flow/test_repo_1_channel.py | 48 +++++++------ ...test_repo_1_channel_branch_update_merge.py | 48 +++++++------ .../github_flow/test_repo_2_channels.py | 48 +++++++------ .../trunk_based_dev/test_repo_trunk.py | 48 +++++++------ .../test_repo_trunk_dual_version_support.py | 48 +++++++------ ...runk_dual_version_support_w_prereleases.py | 49 +++++++------ .../test_repo_trunk_w_prereleases.py | 48 +++++++------ tests/e2e/cmd_version/test_version_bump.py | 68 +++++++++++------- .../e2e/cmd_version/test_version_changelog.py | 69 ++++++++++--------- ...est_version_changelog_custom_commit_msg.py | 26 +++---- .../test_version_github_actions.py | 15 ++-- tests/e2e/cmd_version/test_version_print.py | 56 ++++++++------- tests/e2e/cmd_version/test_version_stamp.py | 3 +- tests/e2e/test_main.py | 3 +- 19 files changed, 420 insertions(+), 353 deletions(-) diff --git a/tests/e2e/cmd_version/bump_version/conftest.py b/tests/e2e/cmd_version/bump_version/conftest.py index d319be2a1..3af0678df 100644 --- a/tests/e2e/cmd_version/bump_version/conftest.py +++ b/tests/e2e/cmd_version/bump_version/conftest.py @@ -1,5 +1,6 @@ from __future__ import annotations +from pathlib import Path from typing import TYPE_CHECKING import pytest @@ -11,7 +12,6 @@ from tests.util import assert_successful_exit_code if TYPE_CHECKING: - from pathlib import Path from typing import Protocol, Sequence from click.testing import Result @@ -63,7 +63,7 @@ def _init_mirror_repo_for_rebuild( else filepath ) if ( - not file.is_relative_to(mirror_git_repo.working_dir) + Path(mirror_git_repo.working_dir) not in file.parents or not file.exists() ): continue diff --git a/tests/e2e/cmd_version/bump_version/git_flow/test_repo_1_channel.py b/tests/e2e/cmd_version/bump_version/git_flow/test_repo_1_channel.py index e257448f8..ad720baf7 100644 --- a/tests/e2e/cmd_version/bump_version/git_flow/test_repo_1_channel.py +++ b/tests/e2e/cmd_version/bump_version/git_flow/test_repo_1_channel.py @@ -1,10 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest -import tomlkit -from flatdict import FlatDict from freezegun import freeze_time from tests.fixtures.repos.git_flow import ( @@ -20,6 +18,8 @@ from requests_mock import Mocker + from semantic_release.version.version import Version + from tests.e2e.cmd_version.bump_version.conftest import ( InitMirrorRepo4RebuildFn, RunPSReleaseFn, @@ -31,7 +31,6 @@ BuildSpecificRepoFn, CommitConvention, GetGitRepo4DirFn, - RepoActionConfigure, RepoActionRelease, RepoActions, SplitRepoActionsByReleaseTagsFn, @@ -60,10 +59,12 @@ def test_gitflow_repo_rebuild_1_channel( build_repo_from_definition: BuildRepoFromDefinitionFn, mocked_git_push: MagicMock, post_mocker: Mocker, - default_tag_format_str: str, version_py_file: Path, get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + pyproject_toml_file: Path, + changelog_md_file: Path, + changelog_rst_file: Path, ): # build target repo into a temporary directory target_repo_dir = example_project_dir / repo_fixture_name @@ -76,30 +77,31 @@ def test_gitflow_repo_rebuild_1_channel( dest_dir=target_repo_dir, ) target_git_repo = git_repo_for_directory(target_repo_dir) - target_repo_pyproject_toml = FlatDict( - tomlkit.loads((target_repo_dir / "pyproject.toml").read_text(encoding="utf-8")), - delimiter=".", - ) - tag_format_str: str = target_repo_pyproject_toml.get( # type: ignore[assignment] - "tool.semantic_release.tag_format", - default_tag_format_str, - ) # split repo actions by release actions - releasetags_2_steps: dict[str, list[RepoActions]] = ( - split_repo_actions_by_release_tags(target_repo_definition, tag_format_str) - ) - configuration_step: RepoActionConfigure = releasetags_2_steps.pop("")[0] # type: ignore[assignment] + organized_steps = split_repo_actions_by_release_tags(target_repo_definition) + configuration_steps = organized_steps.pop(None) + unreleased_steps = organized_steps.pop("Unreleased", None) + if unreleased_steps: + raise ValueError("Unreleased steps found. Not Supported yet!") + + release_tags_2_steps = cast("dict[Version, list[RepoActions]]", organized_steps) # Create the mirror repo directory mirror_repo_dir = init_mirror_repo_for_rebuild( mirror_repo_dir=(example_project_dir / "mirror"), - configuration_step=configuration_step, + configuration_steps=configuration_steps, # type: ignore[arg-type] + files_to_remove=[ + changelog_md_file, + changelog_rst_file, + ], ) mirror_git_repo = git_repo_for_directory(mirror_repo_dir) # rebuild repo from scratch stopping before each release tag - for curr_release_tag, steps in releasetags_2_steps.items(): + for curr_version, steps in release_tags_2_steps.items(): + curr_release_tag = curr_version.as_tag() + # make sure mocks are clear mocked_git_push.reset_mock() post_mocker.reset_mock() @@ -113,7 +115,7 @@ def test_gitflow_repo_rebuild_1_channel( repo_dir=target_repo_dir ) expected_pyproject_toml_content = ( - target_repo_dir / "pyproject.toml" + target_repo_dir / pyproject_toml_file ).read_text() expected_version_file_content = (target_repo_dir / version_py_file).read_text() expected_release_commit_text = target_git_repo.head.commit.message @@ -136,7 +138,9 @@ def test_gitflow_repo_rebuild_1_channel( # take measurement after running the version command actual_release_commit_text = mirror_git_repo.head.commit.message - actual_pyproject_toml_content = (mirror_repo_dir / "pyproject.toml").read_text() + actual_pyproject_toml_content = ( + mirror_repo_dir / pyproject_toml_file + ).read_text() actual_version_file_content = (mirror_repo_dir / version_py_file).read_text() actual_md_changelog_content = get_sanitized_md_changelog_content( repo_dir=mirror_repo_dir @@ -158,4 +162,4 @@ def test_gitflow_repo_rebuild_1_channel( # Make sure tag is created assert curr_release_tag in [tag.name for tag in mirror_git_repo.tags] assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag - assert post_mocker.call_count == 1 # vcs release creation occured + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/bump_version/git_flow/test_repo_2_channels.py b/tests/e2e/cmd_version/bump_version/git_flow/test_repo_2_channels.py index 2f6b30c76..c87a3f2db 100644 --- a/tests/e2e/cmd_version/bump_version/git_flow/test_repo_2_channels.py +++ b/tests/e2e/cmd_version/bump_version/git_flow/test_repo_2_channels.py @@ -1,10 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest -import tomlkit -from flatdict import FlatDict from freezegun import freeze_time from tests.fixtures.repos.git_flow import ( @@ -20,6 +18,8 @@ from requests_mock import Mocker + from semantic_release.version.version import Version + from tests.e2e.cmd_version.bump_version.conftest import ( InitMirrorRepo4RebuildFn, RunPSReleaseFn, @@ -31,7 +31,6 @@ BuildSpecificRepoFn, CommitConvention, GetGitRepo4DirFn, - RepoActionConfigure, RepoActionRelease, RepoActions, SplitRepoActionsByReleaseTagsFn, @@ -60,10 +59,12 @@ def test_gitflow_repo_rebuild_2_channels( build_repo_from_definition: BuildRepoFromDefinitionFn, mocked_git_push: MagicMock, post_mocker: Mocker, - default_tag_format_str: str, version_py_file: Path, get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + pyproject_toml_file: Path, + changelog_md_file: Path, + changelog_rst_file: Path, ): # build target repo into a temporary directory target_repo_dir = example_project_dir / repo_fixture_name @@ -76,30 +77,31 @@ def test_gitflow_repo_rebuild_2_channels( dest_dir=target_repo_dir, ) target_git_repo = git_repo_for_directory(target_repo_dir) - target_repo_pyproject_toml = FlatDict( - tomlkit.loads((target_repo_dir / "pyproject.toml").read_text(encoding="utf-8")), - delimiter=".", - ) - tag_format_str: str = target_repo_pyproject_toml.get( # type: ignore[assignment] - "tool.semantic_release.tag_format", - default_tag_format_str, - ) # split repo actions by release actions - releasetags_2_steps: dict[str, list[RepoActions]] = ( - split_repo_actions_by_release_tags(target_repo_definition, tag_format_str) - ) - configuration_step: RepoActionConfigure = releasetags_2_steps.pop("")[0] # type: ignore[assignment] + organized_steps = split_repo_actions_by_release_tags(target_repo_definition) + configuration_steps = organized_steps.pop(None) + unreleased_steps = organized_steps.pop("Unreleased", None) + if unreleased_steps: + raise ValueError("Unreleased steps found. Not Supported yet!") + + release_tags_2_steps = cast("dict[Version, list[RepoActions]]", organized_steps) # Create the mirror repo directory mirror_repo_dir = init_mirror_repo_for_rebuild( mirror_repo_dir=(example_project_dir / "mirror"), - configuration_step=configuration_step, + configuration_steps=configuration_steps, # type: ignore[arg-type] + files_to_remove=[ + changelog_md_file, + changelog_rst_file, + ], ) mirror_git_repo = git_repo_for_directory(mirror_repo_dir) # rebuild repo from scratch stopping before each release tag - for curr_release_tag, steps in releasetags_2_steps.items(): + for curr_version, steps in release_tags_2_steps.items(): + curr_release_tag = curr_version.as_tag() + # make sure mocks are clear mocked_git_push.reset_mock() post_mocker.reset_mock() @@ -113,7 +115,7 @@ def test_gitflow_repo_rebuild_2_channels( repo_dir=target_repo_dir ) expected_pyproject_toml_content = ( - target_repo_dir / "pyproject.toml" + target_repo_dir / pyproject_toml_file ).read_text() expected_version_file_content = (target_repo_dir / version_py_file).read_text() expected_release_commit_text = target_git_repo.head.commit.message @@ -136,7 +138,9 @@ def test_gitflow_repo_rebuild_2_channels( # take measurement after running the version command actual_release_commit_text = mirror_git_repo.head.commit.message - actual_pyproject_toml_content = (mirror_repo_dir / "pyproject.toml").read_text() + actual_pyproject_toml_content = ( + mirror_repo_dir / pyproject_toml_file + ).read_text() actual_version_file_content = (mirror_repo_dir / version_py_file).read_text() actual_md_changelog_content = get_sanitized_md_changelog_content( repo_dir=mirror_repo_dir @@ -158,4 +162,4 @@ def test_gitflow_repo_rebuild_2_channels( # Make sure tag is created assert curr_release_tag in [tag.name for tag in mirror_git_repo.tags] assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag - assert post_mocker.call_count == 1 # vcs release creation occured + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/bump_version/git_flow/test_repo_3_channels.py b/tests/e2e/cmd_version/bump_version/git_flow/test_repo_3_channels.py index a4dc00675..cadba665a 100644 --- a/tests/e2e/cmd_version/bump_version/git_flow/test_repo_3_channels.py +++ b/tests/e2e/cmd_version/bump_version/git_flow/test_repo_3_channels.py @@ -1,10 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest -import tomlkit -from flatdict import FlatDict from freezegun import freeze_time from tests.fixtures.repos.git_flow import ( @@ -21,6 +19,8 @@ from requests_mock import Mocker + from semantic_release.version.version import Version + from tests.e2e.cmd_version.bump_version.conftest import ( InitMirrorRepo4RebuildFn, RunPSReleaseFn, @@ -32,7 +32,6 @@ BuildSpecificRepoFn, CommitConvention, GetGitRepo4DirFn, - RepoActionConfigure, RepoActionRelease, RepoActions, SplitRepoActionsByReleaseTagsFn, @@ -62,10 +61,12 @@ def test_gitflow_repo_rebuild_3_channels( build_repo_from_definition: BuildRepoFromDefinitionFn, mocked_git_push: MagicMock, post_mocker: Mocker, - default_tag_format_str: str, version_py_file: Path, get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + pyproject_toml_file: Path, + changelog_md_file: Path, + changelog_rst_file: Path, ): # build target repo into a temporary directory target_repo_dir = example_project_dir / repo_fixture_name @@ -78,30 +79,31 @@ def test_gitflow_repo_rebuild_3_channels( dest_dir=target_repo_dir, ) target_git_repo = git_repo_for_directory(target_repo_dir) - target_repo_pyproject_toml = FlatDict( - tomlkit.loads((target_repo_dir / "pyproject.toml").read_text(encoding="utf-8")), - delimiter=".", - ) - tag_format_str: str = target_repo_pyproject_toml.get( # type: ignore[assignment] - "tool.semantic_release.tag_format", - default_tag_format_str, - ) # split repo actions by release actions - releasetags_2_steps: dict[str, list[RepoActions]] = ( - split_repo_actions_by_release_tags(target_repo_definition, tag_format_str) - ) - configuration_step: RepoActionConfigure = releasetags_2_steps.pop("")[0] # type: ignore[assignment] + organized_steps = split_repo_actions_by_release_tags(target_repo_definition) + configuration_steps = organized_steps.pop(None) + unreleased_steps = organized_steps.pop("Unreleased", None) + if unreleased_steps: + raise ValueError("Unreleased steps found. Not Supported yet!") + + release_tags_2_steps = cast("dict[Version, list[RepoActions]]", organized_steps) # Create the mirror repo directory mirror_repo_dir = init_mirror_repo_for_rebuild( mirror_repo_dir=(example_project_dir / "mirror"), - configuration_step=configuration_step, + configuration_steps=configuration_steps, # type: ignore[arg-type] + files_to_remove=[ + changelog_md_file, + changelog_rst_file, + ], ) mirror_git_repo = git_repo_for_directory(mirror_repo_dir) # rebuild repo from scratch stopping before each release tag - for curr_release_tag, steps in releasetags_2_steps.items(): + for curr_version, steps in release_tags_2_steps.items(): + curr_release_tag = curr_version.as_tag() + # make sure mocks are clear mocked_git_push.reset_mock() post_mocker.reset_mock() @@ -115,7 +117,7 @@ def test_gitflow_repo_rebuild_3_channels( repo_dir=target_repo_dir ) expected_pyproject_toml_content = ( - target_repo_dir / "pyproject.toml" + target_repo_dir / pyproject_toml_file ).read_text() expected_version_file_content = (target_repo_dir / version_py_file).read_text() expected_release_commit_text = target_git_repo.head.commit.message @@ -137,7 +139,9 @@ def test_gitflow_repo_rebuild_3_channels( ) # take measurement after running the version command actual_release_commit_text = mirror_git_repo.head.commit.message - actual_pyproject_toml_content = (mirror_repo_dir / "pyproject.toml").read_text() + actual_pyproject_toml_content = ( + mirror_repo_dir / pyproject_toml_file + ).read_text() actual_version_file_content = (mirror_repo_dir / version_py_file).read_text() actual_md_changelog_content = get_sanitized_md_changelog_content( repo_dir=mirror_repo_dir @@ -159,4 +163,4 @@ def test_gitflow_repo_rebuild_3_channels( # Make sure tag is created assert curr_release_tag in [tag.name for tag in mirror_git_repo.tags] assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag - assert post_mocker.call_count == 1 # vcs release creation occured + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/bump_version/git_flow/test_repo_4_channels.py b/tests/e2e/cmd_version/bump_version/git_flow/test_repo_4_channels.py index eeeaa7598..734aeceba 100644 --- a/tests/e2e/cmd_version/bump_version/git_flow/test_repo_4_channels.py +++ b/tests/e2e/cmd_version/bump_version/git_flow/test_repo_4_channels.py @@ -1,10 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest -import tomlkit -from flatdict import FlatDict from freezegun import freeze_time from tests.fixtures.repos.git_flow import ( @@ -20,6 +18,8 @@ from requests_mock import Mocker + from semantic_release.version.version import Version + from tests.e2e.cmd_version.bump_version.conftest import ( InitMirrorRepo4RebuildFn, RunPSReleaseFn, @@ -31,7 +31,6 @@ BuildSpecificRepoFn, CommitConvention, GetGitRepo4DirFn, - RepoActionConfigure, RepoActionRelease, RepoActions, SplitRepoActionsByReleaseTagsFn, @@ -60,10 +59,12 @@ def test_gitflow_repo_rebuild_4_channels( build_repo_from_definition: BuildRepoFromDefinitionFn, mocked_git_push: MagicMock, post_mocker: Mocker, - default_tag_format_str: str, version_py_file: Path, get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + pyproject_toml_file: Path, + changelog_md_file: Path, + changelog_rst_file: Path, ): # build target repo into a temporary directory target_repo_dir = example_project_dir / repo_fixture_name @@ -76,30 +77,31 @@ def test_gitflow_repo_rebuild_4_channels( dest_dir=target_repo_dir, ) target_git_repo = git_repo_for_directory(target_repo_dir) - target_repo_pyproject_toml = FlatDict( - tomlkit.loads((target_repo_dir / "pyproject.toml").read_text(encoding="utf-8")), - delimiter=".", - ) - tag_format_str: str = target_repo_pyproject_toml.get( # type: ignore[assignment] - "tool.semantic_release.tag_format", - default_tag_format_str, - ) # split repo actions by release actions - releasetags_2_steps: dict[str, list[RepoActions]] = ( - split_repo_actions_by_release_tags(target_repo_definition, tag_format_str) - ) - configuration_step: RepoActionConfigure = releasetags_2_steps.pop("")[0] # type: ignore[assignment] + organized_steps = split_repo_actions_by_release_tags(target_repo_definition) + configuration_steps = organized_steps.pop(None) + unreleased_steps = organized_steps.pop("Unreleased", None) + if unreleased_steps: + raise ValueError("Unreleased steps found. Not Supported yet!") + + release_tags_2_steps = cast("dict[Version, list[RepoActions]]", organized_steps) # Create the mirror repo directory mirror_repo_dir = init_mirror_repo_for_rebuild( mirror_repo_dir=(example_project_dir / "mirror"), - configuration_step=configuration_step, + configuration_steps=configuration_steps, # type: ignore[arg-type] + files_to_remove=[ + changelog_md_file, + changelog_rst_file, + ], ) mirror_git_repo = git_repo_for_directory(mirror_repo_dir) # rebuild repo from scratch stopping before each release tag - for curr_release_tag, steps in releasetags_2_steps.items(): + for curr_version, steps in release_tags_2_steps.items(): + curr_release_tag = curr_version.as_tag() + # make sure mocks are clear mocked_git_push.reset_mock() post_mocker.reset_mock() @@ -113,7 +115,7 @@ def test_gitflow_repo_rebuild_4_channels( repo_dir=target_repo_dir ) expected_pyproject_toml_content = ( - target_repo_dir / "pyproject.toml" + target_repo_dir / pyproject_toml_file ).read_text() expected_version_file_content = (target_repo_dir / version_py_file).read_text() expected_release_commit_text = target_git_repo.head.commit.message @@ -136,7 +138,9 @@ def test_gitflow_repo_rebuild_4_channels( # take measurement after running the version command actual_release_commit_text = mirror_git_repo.head.commit.message - actual_pyproject_toml_content = (mirror_repo_dir / "pyproject.toml").read_text() + actual_pyproject_toml_content = ( + mirror_repo_dir / pyproject_toml_file + ).read_text() actual_version_file_content = (mirror_repo_dir / version_py_file).read_text() actual_md_changelog_content = get_sanitized_md_changelog_content( repo_dir=mirror_repo_dir @@ -158,4 +162,4 @@ def test_gitflow_repo_rebuild_4_channels( # Make sure tag is created assert curr_release_tag in [tag.name for tag in mirror_git_repo.tags] assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag - assert post_mocker.call_count == 1 # vcs release creation occured + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/bump_version/github_flow/test_repo_1_channel.py b/tests/e2e/cmd_version/bump_version/github_flow/test_repo_1_channel.py index 2dec4e393..093b80cb3 100644 --- a/tests/e2e/cmd_version/bump_version/github_flow/test_repo_1_channel.py +++ b/tests/e2e/cmd_version/bump_version/github_flow/test_repo_1_channel.py @@ -1,10 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest -import tomlkit -from flatdict import FlatDict from freezegun import freeze_time from tests.fixtures.repos.github_flow import ( @@ -20,6 +18,8 @@ from requests_mock import Mocker + from semantic_release.version.version import Version + from tests.e2e.cmd_version.bump_version.conftest import ( InitMirrorRepo4RebuildFn, RunPSReleaseFn, @@ -31,7 +31,6 @@ BuildSpecificRepoFn, CommitConvention, GetGitRepo4DirFn, - RepoActionConfigure, RepoActionRelease, RepoActions, SplitRepoActionsByReleaseTagsFn, @@ -60,10 +59,12 @@ def test_githubflow_repo_rebuild_1_channel( build_repo_from_definition: BuildRepoFromDefinitionFn, mocked_git_push: MagicMock, post_mocker: Mocker, - default_tag_format_str: str, version_py_file: Path, get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + pyproject_toml_file: Path, + changelog_md_file: Path, + changelog_rst_file: Path, ): # build target repo into a temporary directory target_repo_dir = example_project_dir / repo_fixture_name @@ -76,30 +77,31 @@ def test_githubflow_repo_rebuild_1_channel( dest_dir=target_repo_dir, ) target_git_repo = git_repo_for_directory(target_repo_dir) - target_repo_pyproject_toml = FlatDict( - tomlkit.loads((target_repo_dir / "pyproject.toml").read_text(encoding="utf-8")), - delimiter=".", - ) - tag_format_str: str = target_repo_pyproject_toml.get( # type: ignore[assignment] - "tool.semantic_release.tag_format", - default_tag_format_str, - ) # split repo actions by release actions - releasetags_2_steps: dict[str, list[RepoActions]] = ( - split_repo_actions_by_release_tags(target_repo_definition, tag_format_str) - ) - configuration_step: RepoActionConfigure = releasetags_2_steps.pop("")[0] # type: ignore[assignment] + organized_steps = split_repo_actions_by_release_tags(target_repo_definition) + configuration_steps = organized_steps.pop(None) + unreleased_steps = organized_steps.pop("Unreleased", None) + if unreleased_steps: + raise ValueError("Unreleased steps found. Not Supported yet!") + + release_tags_2_steps = cast("dict[Version, list[RepoActions]]", organized_steps) # Create the mirror repo directory mirror_repo_dir = init_mirror_repo_for_rebuild( mirror_repo_dir=(example_project_dir / "mirror"), - configuration_step=configuration_step, + configuration_steps=configuration_steps, # type: ignore[arg-type] + files_to_remove=[ + changelog_md_file, + changelog_rst_file, + ], ) mirror_git_repo = git_repo_for_directory(mirror_repo_dir) # rebuild repo from scratch stopping before each release tag - for curr_release_tag, steps in releasetags_2_steps.items(): + for curr_version, steps in release_tags_2_steps.items(): + curr_release_tag = curr_version.as_tag() + # make sure mocks are clear mocked_git_push.reset_mock() post_mocker.reset_mock() @@ -113,7 +115,7 @@ def test_githubflow_repo_rebuild_1_channel( repo_dir=target_repo_dir ) expected_pyproject_toml_content = ( - target_repo_dir / "pyproject.toml" + target_repo_dir / pyproject_toml_file ).read_text() expected_version_file_content = (target_repo_dir / version_py_file).read_text() expected_release_commit_text = target_git_repo.head.commit.message @@ -136,7 +138,9 @@ def test_githubflow_repo_rebuild_1_channel( # take measurement after running the version command actual_release_commit_text = mirror_git_repo.head.commit.message - actual_pyproject_toml_content = (mirror_repo_dir / "pyproject.toml").read_text() + actual_pyproject_toml_content = ( + mirror_repo_dir / pyproject_toml_file + ).read_text() actual_version_file_content = (mirror_repo_dir / version_py_file).read_text() actual_md_changelog_content = get_sanitized_md_changelog_content( repo_dir=mirror_repo_dir @@ -158,4 +162,4 @@ def test_githubflow_repo_rebuild_1_channel( # Make sure tag is created assert curr_release_tag in [tag.name for tag in mirror_git_repo.tags] assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag - assert post_mocker.call_count == 1 # vcs release creation occured + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/bump_version/github_flow/test_repo_1_channel_branch_update_merge.py b/tests/e2e/cmd_version/bump_version/github_flow/test_repo_1_channel_branch_update_merge.py index 1c494cfb4..36d156077 100644 --- a/tests/e2e/cmd_version/bump_version/github_flow/test_repo_1_channel_branch_update_merge.py +++ b/tests/e2e/cmd_version/bump_version/github_flow/test_repo_1_channel_branch_update_merge.py @@ -1,10 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest -import tomlkit -from flatdict import FlatDict from freezegun import freeze_time from tests.const import ( @@ -23,6 +21,8 @@ from requests_mock import Mocker + from semantic_release.version.version import Version + from tests.e2e.cmd_version.bump_version.conftest import ( InitMirrorRepo4RebuildFn, RunPSReleaseFn, @@ -34,7 +34,6 @@ BuildSpecificRepoFn, CommitConvention, GetGitRepo4DirFn, - RepoActionConfigure, RepoActionRelease, RepoActions, SplitRepoActionsByReleaseTagsFn, @@ -66,10 +65,12 @@ def test_github_flow_repo_w_default_release_n_branch_update_merge( build_repo_from_definition: BuildRepoFromDefinitionFn, mocked_git_push: MagicMock, post_mocker: Mocker, - default_tag_format_str: str, version_py_file: Path, get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + pyproject_toml_file: Path, + changelog_md_file: Path, + changelog_rst_file: Path, ): # build target repo into a temporary directory target_repo_dir = example_project_dir / repo_fixture_name @@ -84,30 +85,31 @@ def test_github_flow_repo_w_default_release_n_branch_update_merge( ) ) target_git_repo = git_repo_for_directory(target_repo_dir) - target_repo_pyproject_toml = FlatDict( - tomlkit.loads((target_repo_dir / "pyproject.toml").read_text(encoding="utf-8")), - delimiter=".", - ) - tag_format_str: str = target_repo_pyproject_toml.get( # type: ignore[assignment] - "tool.semantic_release.tag_format", - default_tag_format_str, - ) # split repo actions by release actions - releasetags_2_steps: dict[str, list[RepoActions]] = ( - split_repo_actions_by_release_tags(target_repo_definition, tag_format_str) - ) - configuration_step: RepoActionConfigure = releasetags_2_steps.pop("")[0] # type: ignore[assignment] + organized_steps = split_repo_actions_by_release_tags(target_repo_definition) + configuration_steps = organized_steps.pop(None) + unreleased_steps = organized_steps.pop("Unreleased", None) + if unreleased_steps: + raise ValueError("Unreleased steps found. Not Supported yet!") + + release_tags_2_steps = cast("dict[Version, list[RepoActions]]", organized_steps) # Create the mirror repo directory mirror_repo_dir = init_mirror_repo_for_rebuild( mirror_repo_dir=(example_project_dir / "mirror"), - configuration_step=configuration_step, + configuration_steps=configuration_steps, # type: ignore[arg-type] + files_to_remove=[ + changelog_md_file, + changelog_rst_file, + ], ) mirror_git_repo = git_repo_for_directory(mirror_repo_dir) # rebuild repo from scratch stopping before each release tag - for curr_release_tag, steps in releasetags_2_steps.items(): + for curr_version, steps in release_tags_2_steps.items(): + curr_release_tag = curr_version.as_tag() + # make sure mocks are clear mocked_git_push.reset_mock() post_mocker.reset_mock() @@ -126,7 +128,7 @@ def test_github_flow_repo_w_default_release_n_branch_update_merge( repo_dir=target_repo_dir ) expected_pyproject_toml_content = ( - target_repo_dir / "pyproject.toml" + target_repo_dir / pyproject_toml_file ).read_text() expected_version_file_content = (target_repo_dir / version_py_file).read_text() expected_release_commit_text = target_git_repo.head.commit.message @@ -149,7 +151,9 @@ def test_github_flow_repo_w_default_release_n_branch_update_merge( # take measurement after running the version command actual_release_commit_text = mirror_git_repo.head.commit.message - actual_pyproject_toml_content = (mirror_repo_dir / "pyproject.toml").read_text() + actual_pyproject_toml_content = ( + mirror_repo_dir / pyproject_toml_file + ).read_text() actual_version_file_content = (mirror_repo_dir / version_py_file).read_text() actual_md_changelog_content = get_sanitized_md_changelog_content( repo_dir=mirror_repo_dir @@ -171,4 +175,4 @@ def test_github_flow_repo_w_default_release_n_branch_update_merge( # Make sure tag is created assert curr_release_tag in [tag.name for tag in mirror_git_repo.tags] assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag - assert post_mocker.call_count == 1 # vcs release creation occured + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/bump_version/github_flow/test_repo_2_channels.py b/tests/e2e/cmd_version/bump_version/github_flow/test_repo_2_channels.py index 8d2ebd3c3..90b6ea16d 100644 --- a/tests/e2e/cmd_version/bump_version/github_flow/test_repo_2_channels.py +++ b/tests/e2e/cmd_version/bump_version/github_flow/test_repo_2_channels.py @@ -1,10 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest -import tomlkit -from flatdict import FlatDict from freezegun import freeze_time from tests.fixtures.repos.github_flow import ( @@ -20,6 +18,8 @@ from requests_mock import Mocker + from semantic_release.version.version import Version + from tests.e2e.cmd_version.bump_version.conftest import ( InitMirrorRepo4RebuildFn, RunPSReleaseFn, @@ -31,7 +31,6 @@ BuildSpecificRepoFn, CommitConvention, GetGitRepo4DirFn, - RepoActionConfigure, RepoActionRelease, RepoActions, SplitRepoActionsByReleaseTagsFn, @@ -60,10 +59,12 @@ def test_githubflow_repo_rebuild_2_channels( build_repo_from_definition: BuildRepoFromDefinitionFn, mocked_git_push: MagicMock, post_mocker: Mocker, - default_tag_format_str: str, version_py_file: Path, get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + pyproject_toml_file: Path, + changelog_md_file: Path, + changelog_rst_file: Path, ): # build target repo into a temporary directory target_repo_dir = example_project_dir / repo_fixture_name @@ -76,30 +77,31 @@ def test_githubflow_repo_rebuild_2_channels( dest_dir=target_repo_dir, ) target_git_repo = git_repo_for_directory(target_repo_dir) - target_repo_pyproject_toml = FlatDict( - tomlkit.loads((target_repo_dir / "pyproject.toml").read_text(encoding="utf-8")), - delimiter=".", - ) - tag_format_str: str = target_repo_pyproject_toml.get( # type: ignore[assignment] - "tool.semantic_release.tag_format", - default_tag_format_str, - ) # split repo actions by release actions - releasetags_2_steps: dict[str, list[RepoActions]] = ( - split_repo_actions_by_release_tags(target_repo_definition, tag_format_str) - ) - configuration_step: RepoActionConfigure = releasetags_2_steps.pop("")[0] # type: ignore[assignment] + organized_steps = split_repo_actions_by_release_tags(target_repo_definition) + configuration_steps = organized_steps.pop(None) + unreleased_steps = organized_steps.pop("Unreleased", None) + if unreleased_steps: + raise ValueError("Unreleased steps found. Not Supported yet!") + + release_tags_2_steps = cast("dict[Version, list[RepoActions]]", organized_steps) # Create the mirror repo directory mirror_repo_dir = init_mirror_repo_for_rebuild( mirror_repo_dir=(example_project_dir / "mirror"), - configuration_step=configuration_step, + configuration_steps=configuration_steps, # type: ignore[arg-type] + files_to_remove=[ + changelog_md_file, + changelog_rst_file, + ], ) mirror_git_repo = git_repo_for_directory(mirror_repo_dir) # rebuild repo from scratch stopping before each release tag - for curr_release_tag, steps in releasetags_2_steps.items(): + for curr_version, steps in release_tags_2_steps.items(): + curr_release_tag = curr_version.as_tag() + # make sure mocks are clear mocked_git_push.reset_mock() post_mocker.reset_mock() @@ -113,7 +115,7 @@ def test_githubflow_repo_rebuild_2_channels( repo_dir=target_repo_dir ) expected_pyproject_toml_content = ( - target_repo_dir / "pyproject.toml" + target_repo_dir / pyproject_toml_file ).read_text() expected_version_file_content = (target_repo_dir / version_py_file).read_text() expected_release_commit_text = target_git_repo.head.commit.message @@ -136,7 +138,9 @@ def test_githubflow_repo_rebuild_2_channels( # take measurement after running the version command actual_release_commit_text = mirror_git_repo.head.commit.message - actual_pyproject_toml_content = (mirror_repo_dir / "pyproject.toml").read_text() + actual_pyproject_toml_content = ( + mirror_repo_dir / pyproject_toml_file + ).read_text() actual_version_file_content = (mirror_repo_dir / version_py_file).read_text() actual_md_changelog_content = get_sanitized_md_changelog_content( repo_dir=mirror_repo_dir @@ -158,4 +162,4 @@ def test_githubflow_repo_rebuild_2_channels( # Make sure tag is created assert curr_release_tag in [tag.name for tag in mirror_git_repo.tags] assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag - assert post_mocker.call_count == 1 # vcs release creation occured + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk.py b/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk.py index d079b6638..8a68c20de 100644 --- a/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk.py +++ b/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk.py @@ -1,10 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest -import tomlkit -from flatdict import FlatDict from freezegun import freeze_time from tests.fixtures.repos.trunk_based_dev import ( @@ -20,6 +18,8 @@ from requests_mock import Mocker + from semantic_release.version.version import Version + from tests.e2e.cmd_version.bump_version.conftest import ( InitMirrorRepo4RebuildFn, RunPSReleaseFn, @@ -31,7 +31,6 @@ BuildSpecificRepoFn, CommitConvention, GetGitRepo4DirFn, - RepoActionConfigure, RepoActionRelease, RepoActions, SplitRepoActionsByReleaseTagsFn, @@ -62,10 +61,12 @@ def test_trunk_repo_rebuild_only_official_releases( build_repo_from_definition: BuildRepoFromDefinitionFn, mocked_git_push: MagicMock, post_mocker: Mocker, - default_tag_format_str: str, version_py_file: Path, get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + pyproject_toml_file: Path, + changelog_md_file: Path, + changelog_rst_file: Path, ): # build target repo into a temporary directory target_repo_dir = example_project_dir / repo_fixture_name @@ -78,30 +79,31 @@ def test_trunk_repo_rebuild_only_official_releases( dest_dir=target_repo_dir, ) target_git_repo = git_repo_for_directory(target_repo_dir) - target_repo_pyproject_toml = FlatDict( - tomlkit.loads((target_repo_dir / "pyproject.toml").read_text(encoding="utf-8")), - delimiter=".", - ) - tag_format_str: str = target_repo_pyproject_toml.get( # type: ignore[assignment] - "tool.semantic_release.tag_format", - default_tag_format_str, - ) # split repo actions by release actions - releasetags_2_steps: dict[str, list[RepoActions]] = ( - split_repo_actions_by_release_tags(target_repo_definition, tag_format_str) - ) - configuration_step: RepoActionConfigure = releasetags_2_steps.pop("")[0] # type: ignore[assignment] + organized_steps = split_repo_actions_by_release_tags(target_repo_definition) + configuration_steps = organized_steps.pop(None) + unreleased_steps = organized_steps.pop("Unreleased", None) + if unreleased_steps: + raise ValueError("Unreleased steps found. Not Supported yet!") + + release_tags_2_steps = cast("dict[Version, list[RepoActions]]", organized_steps) # Create the mirror repo directory mirror_repo_dir = init_mirror_repo_for_rebuild( mirror_repo_dir=(example_project_dir / "mirror"), - configuration_step=configuration_step, + configuration_steps=configuration_steps, # type: ignore[arg-type] + files_to_remove=[ + changelog_md_file, + changelog_rst_file, + ], ) mirror_git_repo = git_repo_for_directory(mirror_repo_dir) # rebuild repo from scratch stopping before each release tag - for curr_release_tag, steps in releasetags_2_steps.items(): + for curr_version, steps in release_tags_2_steps.items(): + curr_release_tag = curr_version.as_tag() + # make sure mocks are clear mocked_git_push.reset_mock() post_mocker.reset_mock() @@ -115,7 +117,7 @@ def test_trunk_repo_rebuild_only_official_releases( repo_dir=target_repo_dir ) expected_pyproject_toml_content = ( - target_repo_dir / "pyproject.toml" + target_repo_dir / pyproject_toml_file ).read_text() expected_version_file_content = (target_repo_dir / version_py_file).read_text() expected_release_commit_text = target_git_repo.head.commit.message @@ -138,7 +140,9 @@ def test_trunk_repo_rebuild_only_official_releases( # take measurement after running the version command actual_release_commit_text = mirror_git_repo.head.commit.message - actual_pyproject_toml_content = (mirror_repo_dir / "pyproject.toml").read_text() + actual_pyproject_toml_content = ( + mirror_repo_dir / pyproject_toml_file + ).read_text() actual_version_file_content = (mirror_repo_dir / version_py_file).read_text() actual_md_changelog_content = get_sanitized_md_changelog_content( repo_dir=mirror_repo_dir @@ -160,4 +164,4 @@ def test_trunk_repo_rebuild_only_official_releases( # Make sure tag is created assert curr_release_tag in [tag.name for tag in mirror_git_repo.tags] assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag - assert post_mocker.call_count == 1 # vcs release creation occured + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk_dual_version_support.py b/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk_dual_version_support.py index f68bf817e..6236f5b55 100644 --- a/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk_dual_version_support.py +++ b/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk_dual_version_support.py @@ -1,10 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest -import tomlkit -from flatdict import FlatDict from freezegun import freeze_time from tests.const import ( @@ -23,6 +21,8 @@ from requests_mock import Mocker + from semantic_release.version.version import Version + from tests.e2e.cmd_version.bump_version.conftest import ( InitMirrorRepo4RebuildFn, RunPSReleaseFn, @@ -34,7 +34,6 @@ BuildSpecificRepoFn, CommitConvention, GetGitRepo4DirFn, - RepoActionConfigure, RepoActionRelease, RepoActions, SplitRepoActionsByReleaseTagsFn, @@ -63,10 +62,12 @@ def test_trunk_repo_rebuild_dual_version_spt_official_releases_only( build_repo_from_definition: BuildRepoFromDefinitionFn, mocked_git_push: MagicMock, post_mocker: Mocker, - default_tag_format_str: str, version_py_file: Path, get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + pyproject_toml_file: Path, + changelog_md_file: Path, + changelog_rst_file: Path, ): # build target repo into a temporary directory target_repo_dir = example_project_dir / repo_fixture_name @@ -79,30 +80,31 @@ def test_trunk_repo_rebuild_dual_version_spt_official_releases_only( dest_dir=target_repo_dir, ) target_git_repo = git_repo_for_directory(target_repo_dir) - target_repo_pyproject_toml = FlatDict( - tomlkit.loads((target_repo_dir / "pyproject.toml").read_text(encoding="utf-8")), - delimiter=".", - ) - tag_format_str: str = target_repo_pyproject_toml.get( # type: ignore[assignment] - "tool.semantic_release.tag_format", - default_tag_format_str, - ) # split repo actions by release actions - releasetags_2_steps: dict[str, list[RepoActions]] = ( - split_repo_actions_by_release_tags(target_repo_definition, tag_format_str) - ) - configuration_step: RepoActionConfigure = releasetags_2_steps.pop("")[0] # type: ignore[assignment] + organized_steps = split_repo_actions_by_release_tags(target_repo_definition) + configuration_steps = organized_steps.pop(None) + unreleased_steps = organized_steps.pop("Unreleased", None) + if unreleased_steps: + raise ValueError("Unreleased steps found. Not Supported yet!") + + release_tags_2_steps = cast("dict[Version, list[RepoActions]]", organized_steps) # Create the mirror repo directory mirror_repo_dir = init_mirror_repo_for_rebuild( mirror_repo_dir=(example_project_dir / "mirror"), - configuration_step=configuration_step, + configuration_steps=configuration_steps, # type: ignore[arg-type] + files_to_remove=[ + changelog_md_file, + changelog_rst_file, + ], ) mirror_git_repo = git_repo_for_directory(mirror_repo_dir) # rebuild repo from scratch stopping before each release tag - for curr_release_tag, steps in releasetags_2_steps.items(): + for curr_version, steps in release_tags_2_steps.items(): + curr_release_tag = curr_version.as_tag() + # make sure mocks are clear mocked_git_push.reset_mock() post_mocker.reset_mock() @@ -121,7 +123,7 @@ def test_trunk_repo_rebuild_dual_version_spt_official_releases_only( repo_dir=target_repo_dir ) expected_pyproject_toml_content = ( - target_repo_dir / "pyproject.toml" + target_repo_dir / pyproject_toml_file ).read_text() expected_version_file_content = (target_repo_dir / version_py_file).read_text() expected_release_commit_text = target_git_repo.head.commit.message @@ -144,7 +146,9 @@ def test_trunk_repo_rebuild_dual_version_spt_official_releases_only( # take measurement after running the version command actual_release_commit_text = mirror_git_repo.head.commit.message - actual_pyproject_toml_content = (mirror_repo_dir / "pyproject.toml").read_text() + actual_pyproject_toml_content = ( + mirror_repo_dir / pyproject_toml_file + ).read_text() actual_version_file_content = (mirror_repo_dir / version_py_file).read_text() actual_md_changelog_content = get_sanitized_md_changelog_content( repo_dir=mirror_repo_dir @@ -166,4 +170,4 @@ def test_trunk_repo_rebuild_dual_version_spt_official_releases_only( # Make sure tag is created assert curr_release_tag in [tag.name for tag in mirror_git_repo.tags] assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag - assert post_mocker.call_count == 1 # vcs release creation occured + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk_dual_version_support_w_prereleases.py b/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk_dual_version_support_w_prereleases.py index 1514dac38..020a4e6ac 100644 --- a/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk_dual_version_support_w_prereleases.py +++ b/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk_dual_version_support_w_prereleases.py @@ -1,10 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest -import tomlkit -from flatdict import FlatDict from freezegun import freeze_time from tests.const import ( @@ -23,6 +21,8 @@ from requests_mock import Mocker + from semantic_release.version.version import Version + from tests.e2e.cmd_version.bump_version.conftest import ( InitMirrorRepo4RebuildFn, RunPSReleaseFn, @@ -34,13 +34,13 @@ BuildSpecificRepoFn, CommitConvention, GetGitRepo4DirFn, - RepoActionConfigure, RepoActionRelease, RepoActions, SplitRepoActionsByReleaseTagsFn, ) +@pytest.mark.xfail(reason="Not yet implemented, see issue #555 for details") @pytest.mark.parametrize( "repo_fixture_name", [ @@ -63,10 +63,12 @@ def test_trunk_repo_rebuild_dual_version_spt_w_official_n_prereleases( build_repo_from_definition: BuildRepoFromDefinitionFn, mocked_git_push: MagicMock, post_mocker: Mocker, - default_tag_format_str: str, version_py_file: Path, get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + pyproject_toml_file: Path, + changelog_md_file: Path, + changelog_rst_file: Path, ): # build target repo into a temporary directory target_repo_dir = example_project_dir / repo_fixture_name @@ -79,30 +81,31 @@ def test_trunk_repo_rebuild_dual_version_spt_w_official_n_prereleases( dest_dir=target_repo_dir, ) target_git_repo = git_repo_for_directory(target_repo_dir) - target_repo_pyproject_toml = FlatDict( - tomlkit.loads((target_repo_dir / "pyproject.toml").read_text(encoding="utf-8")), - delimiter=".", - ) - tag_format_str: str = target_repo_pyproject_toml.get( # type: ignore[assignment] - "tool.semantic_release.tag_format", - default_tag_format_str, - ) # split repo actions by release actions - releasetags_2_steps: dict[str, list[RepoActions]] = ( - split_repo_actions_by_release_tags(target_repo_definition, tag_format_str) - ) - configuration_step: RepoActionConfigure = releasetags_2_steps.pop("")[0] # type: ignore[assignment] + organized_steps = split_repo_actions_by_release_tags(target_repo_definition) + configuration_steps = organized_steps.pop(None) + unreleased_steps = organized_steps.pop("Unreleased", None) + if unreleased_steps: + raise ValueError("Unreleased steps found. Not Supported yet!") + + release_tags_2_steps = cast("dict[Version, list[RepoActions]]", organized_steps) # Create the mirror repo directory mirror_repo_dir = init_mirror_repo_for_rebuild( mirror_repo_dir=(example_project_dir / "mirror"), - configuration_step=configuration_step, + configuration_steps=configuration_steps, # type: ignore[arg-type] + files_to_remove=[ + changelog_md_file, + changelog_rst_file, + ], ) mirror_git_repo = git_repo_for_directory(mirror_repo_dir) # rebuild repo from scratch stopping before each release tag - for curr_release_tag, steps in releasetags_2_steps.items(): + for curr_version, steps in release_tags_2_steps.items(): + curr_release_tag = curr_version.as_tag() + # make sure mocks are clear mocked_git_push.reset_mock() post_mocker.reset_mock() @@ -121,7 +124,7 @@ def test_trunk_repo_rebuild_dual_version_spt_w_official_n_prereleases( repo_dir=target_repo_dir ) expected_pyproject_toml_content = ( - target_repo_dir / "pyproject.toml" + target_repo_dir / pyproject_toml_file ).read_text() expected_version_file_content = (target_repo_dir / version_py_file).read_text() expected_release_commit_text = target_git_repo.head.commit.message @@ -144,7 +147,9 @@ def test_trunk_repo_rebuild_dual_version_spt_w_official_n_prereleases( # take measurement after running the version command actual_release_commit_text = mirror_git_repo.head.commit.message - actual_pyproject_toml_content = (mirror_repo_dir / "pyproject.toml").read_text() + actual_pyproject_toml_content = ( + mirror_repo_dir / pyproject_toml_file + ).read_text() actual_version_file_content = (mirror_repo_dir / version_py_file).read_text() actual_md_changelog_content = get_sanitized_md_changelog_content( repo_dir=mirror_repo_dir @@ -166,4 +171,4 @@ def test_trunk_repo_rebuild_dual_version_spt_w_official_n_prereleases( # Make sure tag is created assert curr_release_tag in [tag.name for tag in mirror_git_repo.tags] assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag - assert post_mocker.call_count == 1 # vcs release creation occured + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk_w_prereleases.py b/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk_w_prereleases.py index 67af5d56a..4473f56e9 100644 --- a/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk_w_prereleases.py +++ b/tests/e2e/cmd_version/bump_version/trunk_based_dev/test_repo_trunk_w_prereleases.py @@ -1,10 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest -import tomlkit -from flatdict import FlatDict from freezegun import freeze_time from tests.fixtures.repos.trunk_based_dev import ( @@ -20,6 +18,8 @@ from requests_mock import Mocker + from semantic_release.version.version import Version + from tests.e2e.cmd_version.bump_version.conftest import ( InitMirrorRepo4RebuildFn, RunPSReleaseFn, @@ -31,7 +31,6 @@ BuildSpecificRepoFn, CommitConvention, GetGitRepo4DirFn, - RepoActionConfigure, RepoActionRelease, RepoActions, SplitRepoActionsByReleaseTagsFn, @@ -60,10 +59,12 @@ def test_trunk_repo_rebuild_w_prereleases( build_repo_from_definition: BuildRepoFromDefinitionFn, mocked_git_push: MagicMock, post_mocker: Mocker, - default_tag_format_str: str, version_py_file: Path, get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + pyproject_toml_file: Path, + changelog_md_file: Path, + changelog_rst_file: Path, ): # build target repo into a temporary directory target_repo_dir = example_project_dir / repo_fixture_name @@ -76,30 +77,31 @@ def test_trunk_repo_rebuild_w_prereleases( dest_dir=target_repo_dir, ) target_git_repo = git_repo_for_directory(target_repo_dir) - target_repo_pyproject_toml = FlatDict( - tomlkit.loads((target_repo_dir / "pyproject.toml").read_text(encoding="utf-8")), - delimiter=".", - ) - tag_format_str: str = target_repo_pyproject_toml.get( # type: ignore[assignment] - "tool.semantic_release.tag_format", - default_tag_format_str, - ) # split repo actions by release actions - releasetags_2_steps: dict[str, list[RepoActions]] = ( - split_repo_actions_by_release_tags(target_repo_definition, tag_format_str) - ) - configuration_step: RepoActionConfigure = releasetags_2_steps.pop("")[0] # type: ignore[assignment] + organized_steps = split_repo_actions_by_release_tags(target_repo_definition) + configuration_steps = organized_steps.pop(None) + unreleased_steps = organized_steps.pop("Unreleased", None) + if unreleased_steps: + raise ValueError("Unreleased steps found. Not Supported yet!") + + release_tags_2_steps = cast("dict[Version, list[RepoActions]]", organized_steps) # Create the mirror repo directory mirror_repo_dir = init_mirror_repo_for_rebuild( mirror_repo_dir=(example_project_dir / "mirror"), - configuration_step=configuration_step, + configuration_steps=configuration_steps, # type: ignore[arg-type] + files_to_remove=[ + changelog_md_file, + changelog_rst_file, + ], ) mirror_git_repo = git_repo_for_directory(mirror_repo_dir) # rebuild repo from scratch stopping before each release tag - for curr_release_tag, steps in releasetags_2_steps.items(): + for curr_version, steps in release_tags_2_steps.items(): + curr_release_tag = curr_version.as_tag() + # make sure mocks are clear mocked_git_push.reset_mock() post_mocker.reset_mock() @@ -113,7 +115,7 @@ def test_trunk_repo_rebuild_w_prereleases( repo_dir=target_repo_dir ) expected_pyproject_toml_content = ( - target_repo_dir / "pyproject.toml" + target_repo_dir / pyproject_toml_file ).read_text() expected_version_file_content = (target_repo_dir / version_py_file).read_text() expected_release_commit_text = target_git_repo.head.commit.message @@ -136,7 +138,9 @@ def test_trunk_repo_rebuild_w_prereleases( # take measurement after running the version command actual_release_commit_text = mirror_git_repo.head.commit.message - actual_pyproject_toml_content = (mirror_repo_dir / "pyproject.toml").read_text() + actual_pyproject_toml_content = ( + mirror_repo_dir / pyproject_toml_file + ).read_text() actual_version_file_content = (mirror_repo_dir / version_py_file).read_text() actual_md_changelog_content = get_sanitized_md_changelog_content( repo_dir=mirror_repo_dir @@ -158,4 +162,4 @@ def test_trunk_repo_rebuild_w_prereleases( # Make sure tag is created assert curr_release_tag in [tag.name for tag in mirror_git_repo.tags] assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag - assert post_mocker.call_count == 1 # vcs release creation occured + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/test_version_bump.py b/tests/e2e/cmd_version/test_version_bump.py index c08efb9ee..5feade30c 100644 --- a/tests/e2e/cmd_version/test_version_bump.py +++ b/tests/e2e/cmd_version/test_version_bump.py @@ -3,7 +3,7 @@ from datetime import timedelta from itertools import count from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest import tomlkit @@ -61,7 +61,11 @@ from requests_mock import Mocker from tests.conftest import GetStableDateNowFn, RunCliFn - from tests.fixtures.example_project import ExProjectDir, UpdatePyprojectTomlFn + from tests.fixtures.example_project import ( + ExProjectDir, + GetExpectedVersionPyFileContentFn, + UpdatePyprojectTomlFn, + ) from tests.fixtures.git_repo import BuiltRepoResult @@ -319,25 +323,33 @@ def test_version_force_level( run_cli: RunCliFn, mocked_git_push: MagicMock, post_mocker: Mocker, + pyproject_toml_file: Path, + changelog_md_file: Path, + get_expected_version_py_file_content: GetExpectedVersionPyFileContentFn, ): + # Force clean directory state before test (needed for the repo_w_no_tags) repo = repo_result["repo"] + repo.git.reset("HEAD", hard=True) + version_file = example_project_dir.joinpath( "src", EXAMPLE_PROJECT_NAME, "_version.py" ) + expected_changed_files = sorted( [ - "CHANGELOG.md", - "pyproject.toml", + str(changelog_md_file), + str(pyproject_toml_file), str(version_file.relative_to(example_project_dir)), ] ) + expected_version_py_content = get_expected_version_py_file_content( + next_release_version + ) + # Setup: take measurement before running the version command head_sha_before = repo.head.commit.hexsha tags_before = {tag.name for tag in repo.tags} - version_py_before = dynamic_python_import( - version_file, f"{EXAMPLE_PROJECT_NAME}._version" - ).__version__ pyproject_toml_before = tomlkit.loads( example_pyproject_toml.read_text(encoding="utf-8") @@ -353,7 +365,7 @@ def test_version_force_level( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) differing_files = [ # Make sure filepath uses os specific path separators str(Path(file)) @@ -367,9 +379,7 @@ def test_version_force_level( ) # Load python module for reading the version (ensures the file is valid) - version_py_after = dynamic_python_import( - version_file, f"{EXAMPLE_PROJECT_NAME}._version" - ).__version__ + actual_version_py_content = version_file.read_text() # Evaluate (normal release actions should have occurred when forced patch bump) assert_successful_exit_code(result, cli_cmd) @@ -391,8 +401,14 @@ def test_version_force_level( assert next_release_version == pyproj_version_after # Compare _version.py - assert next_release_version == version_py_after - assert version_py_before != version_py_after + assert expected_version_py_content == actual_version_py_content + + # Verify content is parsable & importable + dynamic_version = dynamic_python_import( + version_file, f"{EXAMPLE_PROJECT_NAME}._version" + ).__version__ + + assert next_release_version == dynamic_version # NOTE: There is a bit of a corner-case where if we are not doing a @@ -560,7 +576,7 @@ def test_version_next_greater_than_version_one_conventional( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (normal release actions should have occurred when forced patch bump) assert_successful_exit_code(result, cli_cmd) @@ -699,7 +715,7 @@ def test_version_next_greater_than_version_one_no_bump_conventional( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (no release actions should have occurred when no bump) assert_successful_exit_code(result, cli_cmd) @@ -859,7 +875,7 @@ def test_version_next_greater_than_version_one_emoji( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (normal release actions should have occurred when forced patch bump) assert_successful_exit_code(result, cli_cmd) @@ -998,7 +1014,7 @@ def test_version_next_greater_than_version_one_no_bump_emoji( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (normal release actions should have occurred when forced patch bump) assert_successful_exit_code(result, cli_cmd) @@ -1158,7 +1174,7 @@ def test_version_next_greater_than_version_one_scipy( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (normal release actions should have occurred when forced patch bump) assert_successful_exit_code(result, cli_cmd) @@ -1297,7 +1313,7 @@ def test_version_next_greater_than_version_one_no_bump_scipy( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (no release actions should have occurred when no bump) assert_successful_exit_code(result, cli_cmd) @@ -1644,7 +1660,7 @@ def test_version_next_w_zero_dot_versions_conventional( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (normal release actions should have occurred when forced patch bump) assert_successful_exit_code(result, cli_cmd) @@ -1798,7 +1814,7 @@ def test_version_next_w_zero_dot_versions_no_bump_conventional( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (no release actions should have occurred when no bump) assert_successful_exit_code(result, cli_cmd) @@ -2124,7 +2140,7 @@ def test_version_next_w_zero_dot_versions_emoji( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (normal release actions should have occurred when forced patch bump) assert_successful_exit_code(result, cli_cmd) @@ -2278,7 +2294,7 @@ def test_version_next_w_zero_dot_versions_no_bump_emoji( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (no release actions should have occurred when no bump) assert_successful_exit_code(result, cli_cmd) @@ -2604,7 +2620,7 @@ def test_version_next_w_zero_dot_versions_scipy( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (normal release actions should have occurred when forced patch bump) assert_successful_exit_code(result, cli_cmd) @@ -2758,7 +2774,7 @@ def test_version_next_w_zero_dot_versions_no_bump_scipy( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (no release actions should have occurred when no bump) assert_successful_exit_code(result, cli_cmd) @@ -3148,7 +3164,7 @@ def test_version_next_w_zero_dot_versions_minimums( # take measurement after running the version command head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (normal release actions should have occurred when forced patch bump) assert_successful_exit_code(result, cli_cmd) diff --git a/tests/e2e/cmd_version/test_version_changelog.py b/tests/e2e/cmd_version/test_version_changelog.py index a2ce88bf4..8ef83f9af 100644 --- a/tests/e2e/cmd_version/test_version_changelog.py +++ b/tests/e2e/cmd_version/test_version_changelog.py @@ -48,7 +48,12 @@ if TYPE_CHECKING: from pathlib import Path - from tests.conftest import FormatDateStrFn, GetStableDateNowFn, RunCliFn + from tests.conftest import ( + FormatDateStrFn, + GetCachedRepoDataFn, + GetStableDateNowFn, + RunCliFn, + ) from tests.fixtures.example_project import UpdatePyprojectTomlFn from tests.fixtures.git_repo import ( BuiltRepoResult, @@ -74,17 +79,17 @@ ], ) @pytest.mark.parametrize( - "repo_result, cache_key, tag_format", + "repo_result, repo_fixture_name, tag_format", [ ( lazy_fixture(repo_w_trunk_only_conventional_commits.__name__), - f"psr/repos/{repo_w_trunk_only_conventional_commits.__name__}", + repo_w_trunk_only_conventional_commits.__name__, "v{version}", ), *[ pytest.param( lazy_fixture(repo_fixture), - f"psr/repos/{repo_fixture}", + repo_fixture, "v{version}" if tag_format is None else tag_format, marks=pytest.mark.comprehensive, ) @@ -177,9 +182,9 @@ def test_version_updates_changelog_w_new_version( run_cli: RunCliFn, changelog_file: Path, insertion_flag: str, - cache: pytest.Cache, - cache_key: str, + repo_fixture_name: str, stable_now_date: GetStableDateNowFn, + get_cached_repo_data: GetCachedRepoDataFn, ): """ Given a previously released custom modified changelog file, @@ -192,7 +197,7 @@ def test_version_updates_changelog_w_new_version( version=get_versions_from_repo_build_def(repo_result["definition"])[-1] ) - if not (repo_build_data := cache.get(cache_key, None)): + if not (repo_build_data := get_cached_repo_data(repo_fixture_name)): pytest.fail("Repo build date not found in cache") repo_build_datetime = datetime.strptime(repo_build_data["build_date"], "%Y-%m-%d") @@ -280,16 +285,16 @@ def test_version_updates_changelog_w_new_version( ], ) @pytest.mark.parametrize( - "repo_result, cache_key", + "repo_result, repo_fixture_name", [ ( lazy_fixture(repo_w_no_tags_conventional_commits.__name__), - f"psr/repos/{repo_w_no_tags_conventional_commits.__name__}", + repo_w_no_tags_conventional_commits.__name__, ), *[ pytest.param( lazy_fixture(repo_fixture), - f"psr/repos/{repo_fixture}", + repo_fixture, marks=pytest.mark.comprehensive, ) for repo_fixture in [ @@ -303,8 +308,7 @@ def test_version_updates_changelog_w_new_version( ) def test_version_updates_changelog_wo_prev_releases( repo_result: BuiltRepoResult, - cache_key: str, - cache: pytest.Cache, + repo_fixture_name: str, run_cli: RunCliFn, update_pyproject_toml: UpdatePyprojectTomlFn, changelog_format: ChangelogOutputFormat, @@ -312,13 +316,14 @@ def test_version_updates_changelog_wo_prev_releases( insertion_flag: str, stable_now_date: GetStableDateNowFn, format_date_str: FormatDateStrFn, + get_cached_repo_data: GetCachedRepoDataFn, ): """ Given the repository has no releases and the user has provided a initialized changelog, When the version command is run with changelog.mode set to "update", Then the version is created and the changelog file is updated with only an initial release statement """ - if not (repo_build_data := cache.get(cache_key, None)): + if not (repo_build_data := get_cached_repo_data(repo_fixture_name)): pytest.fail("Repo build date not found in cache") repo_build_datetime = datetime.strptime(repo_build_data["build_date"], "%Y-%m-%d") @@ -433,11 +438,11 @@ def test_version_updates_changelog_wo_prev_releases( ], ) @pytest.mark.parametrize( - "repo_result, cache_key", + "repo_result, repo_fixture_name", [ pytest.param( lazy_fixture(repo_fixture), - f"psr/repos/{repo_fixture}", + repo_fixture, marks=pytest.mark.comprehensive, ) for repo_fixture in [ @@ -448,8 +453,7 @@ def test_version_updates_changelog_wo_prev_releases( ) def test_version_updates_changelog_wo_prev_releases_n_unmasked_initial_release( repo_result: BuiltRepoResult, - cache_key: str, - cache: pytest.Cache, + repo_fixture_name: str, run_cli: RunCliFn, update_pyproject_toml: UpdatePyprojectTomlFn, changelog_format: ChangelogOutputFormat, @@ -457,13 +461,14 @@ def test_version_updates_changelog_wo_prev_releases_n_unmasked_initial_release( insertion_flag: str, stable_now_date: GetStableDateNowFn, format_date_str: FormatDateStrFn, + get_cached_repo_data: GetCachedRepoDataFn, ): """ Given the repository has no releases and the user has provided a initialized changelog, When the version command is run with changelog.mode set to "update", Then the version is created and the changelog file is updated with new release info """ - if not (repo_build_data := cache.get(cache_key, None)): + if not (repo_build_data := get_cached_repo_data(repo_fixture_name)): pytest.fail("Repo build date not found in cache") repo_build_datetime = datetime.strptime(repo_build_data["build_date"], "%Y-%m-%d") @@ -537,7 +542,7 @@ def test_version_updates_changelog_wo_prev_releases_n_unmasked_initial_release( ], ) - # Grab the Unreleased changelog & create the initalized user changelog + # Grab the Unreleased changelog & create the initialized user changelog # force output to not perform any newline translations with changelog_file.open(mode="w", newline="") as wfd: wfd.write( @@ -575,17 +580,17 @@ def test_version_updates_changelog_wo_prev_releases_n_unmasked_initial_release( ], ) @pytest.mark.parametrize( - "repo_result, cache_key, tag_format", + "repo_result, repo_fixture_name, tag_format", [ ( lazy_fixture(repo_w_trunk_only_conventional_commits.__name__), - f"psr/repos/{repo_w_trunk_only_conventional_commits.__name__}", + repo_w_trunk_only_conventional_commits.__name__, "v{version}", ), *[ pytest.param( lazy_fixture(repo_fixture), - f"psr/repos/{repo_fixture}", + repo_fixture, "v{version}" if tag_format is None else tag_format, marks=pytest.mark.comprehensive, ) @@ -672,14 +677,14 @@ def test_version_updates_changelog_wo_prev_releases_n_unmasked_initial_release( ) def test_version_initializes_changelog_in_update_mode_w_no_prev_changelog( repo_result: BuiltRepoResult, - cache_key: str, + repo_fixture_name: str, get_versions_from_repo_build_def: GetVersionsFromRepoBuildDefFn, tag_format: str, run_cli: RunCliFn, update_pyproject_toml: UpdatePyprojectTomlFn, changelog_file: Path, - cache: pytest.Cache, stable_now_date: GetStableDateNowFn, + get_cached_repo_data: GetCachedRepoDataFn, ): """ Given that the changelog file does not exist, @@ -692,7 +697,7 @@ def test_version_initializes_changelog_in_update_mode_w_no_prev_changelog( version=get_versions_from_repo_build_def(repo_result["definition"])[-1] ) - if not (repo_build_data := cache.get(cache_key, None)): + if not (repo_build_data := get_cached_repo_data(repo_fixture_name)): pytest.fail("Repo build date not found in cache") repo_build_datetime = datetime.strptime(repo_build_data["build_date"], "%Y-%m-%d") @@ -808,11 +813,11 @@ def test_version_maintains_changelog_in_update_mode_w_no_flag( ], ) @pytest.mark.parametrize( - "repo_result, cache_key, commit_type, tag_format", + "repo_result, repo_fixture_name, commit_type, tag_format", [ ( lazy_fixture(repo_fixture), - f"psr/repos/{repo_fixture}", + repo_fixture, repo_fixture.split("_")[-2], "v{version}", ) @@ -824,8 +829,7 @@ def test_version_maintains_changelog_in_update_mode_w_no_flag( ) def test_version_updates_changelog_w_new_version_n_filtered_commit( repo_result: BuiltRepoResult, - cache: pytest.Cache, - cache_key: str, + repo_fixture_name: str, get_versions_from_repo_build_def: GetVersionsFromRepoBuildDefFn, commit_type: CommitConvention, tag_format: str, @@ -834,6 +838,7 @@ def test_version_updates_changelog_w_new_version_n_filtered_commit( changelog_file: Path, stable_now_date: GetStableDateNowFn, get_commits_from_repo_build_def: GetCommitsFromRepoBuildDefFn, + get_cached_repo_data: GetCachedRepoDataFn, ): """ Given a project that has a version bumping change but also an exclusion pattern for the same change type, @@ -844,9 +849,9 @@ def test_version_updates_changelog_w_new_version_n_filtered_commit( repo = repo_result["repo"] latest_version = get_versions_from_repo_build_def(repo_result["definition"])[-1] latest_tag = tag_format.format(version=latest_version) - repo_definition = get_commits_from_repo_build_def(repo_result["definition"]) - if not (repo_build_data := cache.get(cache_key, None)): + + if not (repo_build_data := get_cached_repo_data(repo_fixture_name)): pytest.fail("Repo build date not found in cache") repo_build_datetime = datetime.strptime(repo_build_data["build_date"], "%Y-%m-%d") @@ -857,7 +862,7 @@ def test_version_updates_changelog_w_new_version_n_filtered_commit( ) # expected version bump commit (that should be in changelog) - bumping_commit = repo_definition[latest_version]["commits"][-1] + bumping_commit = repo_definition[str(latest_version)]["commits"][-1] expected_bump_message = bumping_commit["desc"].capitalize() # Capture the expected changelog content diff --git a/tests/e2e/cmd_version/test_version_changelog_custom_commit_msg.py b/tests/e2e/cmd_version/test_version_changelog_custom_commit_msg.py index acff3e728..00d0b1d18 100644 --- a/tests/e2e/cmd_version/test_version_changelog_custom_commit_msg.py +++ b/tests/e2e/cmd_version/test_version_changelog_custom_commit_msg.py @@ -34,7 +34,7 @@ from pathlib import Path from typing import TypedDict - from tests.conftest import GetStableDateNowFn, RunCliFn + from tests.conftest import GetCachedRepoDataFn, GetStableDateNowFn, RunCliFn from tests.e2e.conftest import GetSanitizedChangelogContentFn from tests.fixtures.example_project import UpdatePyprojectTomlFn from tests.fixtures.git_repo import ( @@ -43,7 +43,6 @@ CommitDef, GetCfgValueFromDefFn, GetVersionsFromRepoBuildDefFn, - RepoActions, SplitRepoActionsByReleaseTagsFn, ) @@ -66,7 +65,7 @@ class Commit2SectionCommit(TypedDict): "changelog_file", "get_sanitized_changelog_content", "repo_result", - "cache_key", + "repo_fixture_name", ], ), [ @@ -76,7 +75,7 @@ class Commit2SectionCommit(TypedDict): lazy_fixture(changelog_file), lazy_fixture(cl_sanitizer), lazy_fixture(repo_fixture_name), - f"psr/repos/{repo_fixture_name}", + repo_fixture_name, marks=pytest.mark.comprehensive, ) for changelog_mode in [ChangelogMode.INIT, ChangelogMode.UPDATE] @@ -122,11 +121,11 @@ def test_version_changelog_content_custom_commit_message_excluded_automatically( changelog_file: Path, changelog_mode: ChangelogMode, custom_commit_message: str, - cache: pytest.Cache, - cache_key: str, + repo_fixture_name: str, stable_now_date: GetStableDateNowFn, example_project_dir: Path, get_sanitized_changelog_content: GetSanitizedChangelogContentFn, + get_cached_repo_data: GetCachedRepoDataFn, ): """ Given a repo with a custom release commit message @@ -145,16 +144,14 @@ def test_version_changelog_content_custom_commit_message_excluded_automatically( repo_def = repo_result["definition"] tag_format_str: str = get_cfg_value_from_def(repo_def, "tag_format_str") # type: ignore[assignment] all_versions = get_versions_from_repo_build_def(repo_def) - latest_tag = tag_format_str.format(version=all_versions[-1]) + latest_version = all_versions[-1] previous_tag = tag_format_str.format(version=all_versions[-2]) # split repo actions by release actions - releasetags_2_steps: dict[str, list[RepoActions]] = ( - split_repo_actions_by_release_tags(repo_def, tag_format_str) - ) + releasetags_2_steps = split_repo_actions_by_release_tags(repo_def) # Reverse release to make the previous version again with the new commit message - repo.git.tag("-d", latest_tag) + repo.git.tag("-d", latest_version.as_tag()) repo.git.reset("--hard", f"{previous_tag}~1") repo.git.tag("-d", previous_tag) @@ -169,7 +166,7 @@ def test_version_changelog_content_custom_commit_message_excluded_automatically( custom_commit_message, ) - if not (repo_build_data := cache.get(cache_key, None)): + if not (repo_build_data := get_cached_repo_data(repo_fixture_name)): pytest.fail("Repo build date not found in cache") repo_build_datetime = datetime.strptime(repo_build_data["build_date"], "%Y-%m-%d") @@ -193,9 +190,8 @@ def test_version_changelog_content_custom_commit_message_excluded_automatically( assert_successful_exit_code(result, cli_cmd) # Act: apply commits for change of version - steps_for_next_release = releasetags_2_steps[latest_tag][ - :-1 - ] # stop before the release step + # stop before the release step + steps_for_next_release = releasetags_2_steps[latest_version][:-1] build_repo_from_definition( dest_dir=example_project_dir, repo_construction_steps=steps_for_next_release, diff --git a/tests/e2e/cmd_version/test_version_github_actions.py b/tests/e2e/cmd_version/test_version_github_actions.py index ab86e556b..b3953f43e 100644 --- a/tests/e2e/cmd_version/test_version_github_actions.py +++ b/tests/e2e/cmd_version/test_version_github_actions.py @@ -8,8 +8,6 @@ from freezegun import freeze_time from pytest_lazy_fixtures.lazy_fixture import lf as lazy_fixture -from semantic_release.version.version import Version - from tests.const import EXAMPLE_PROJECT_LICENSE, MAIN_PROG_NAME, VERSION_SUBCMD from tests.fixtures.repos import ( repo_w_git_flow_w_alpha_prereleases_n_conventional_commits, @@ -52,13 +50,10 @@ def test_version_writes_github_actions_output( all_versions = get_versions_from_repo_build_def(repo_def) latest_release_version = all_versions[-1] release_tag = tag_format_str.format(version=latest_release_version) - previous_version = ( - Version.parse(all_versions[-2]) if len(all_versions) > 1 else None - ) + previous_version = all_versions[-2] if len(all_versions) > 1 else None hvcs_client = cast("Github", get_hvcs_client_from_repo_def(repo_def)) repo_actions_per_version = split_repo_actions_by_release_tags( - repo_definition=repo_def, - tag_format_str=tag_format_str, + repo_definition=repo_def ) expected_gha_output = { "released": str(True).lower(), @@ -66,12 +61,10 @@ def test_version_writes_github_actions_output( "tag": release_tag, "link": hvcs_client.create_release_url(release_tag), "commit_sha": "0" * 40, - "is_prerelease": str( - Version.parse(latest_release_version).is_prerelease - ).lower(), + "is_prerelease": str(latest_release_version.is_prerelease).lower(), "previous_version": str(previous_version) if previous_version else "", "release_notes": generate_default_release_notes_from_def( - version_actions=repo_actions_per_version[release_tag], + version_actions=repo_actions_per_version[latest_release_version], hvcs=hvcs_client, previous_version=previous_version, license_name=EXAMPLE_PROJECT_LICENSE, diff --git a/tests/e2e/cmd_version/test_version_print.py b/tests/e2e/cmd_version/test_version_print.py index 18c0fc5f7..9bcc2dea8 100644 --- a/tests/e2e/cmd_version/test_version_print.py +++ b/tests/e2e/cmd_version/test_version_print.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest from pytest_lazy_fixtures.lazy_fixture import lf as lazy_fixture @@ -11,7 +11,10 @@ MAIN_PROG_NAME, VERSION_SUBCMD, ) -from tests.fixtures.commit_parsers import conventional_minor_commits +from tests.fixtures.commit_parsers import ( + conventional_minor_commits, + default_conventional_parser, +) from tests.fixtures.git_repo import get_commit_def_of_conventional_commit from tests.fixtures.repos import ( repo_w_git_flow_w_rc_n_alpha_prereleases_n_conventional_commits_using_tag_format, @@ -34,6 +37,9 @@ from requests_mock import Mocker + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.token import ParseResult + from tests.conftest import RunCliFn from tests.e2e.conftest import StripLoggingMessagesFn from tests.fixtures.git_repo import ( @@ -136,7 +142,7 @@ def test_version_print_next_version( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit.hexsha tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate assert_successful_exit_code(result, cli_cmd) @@ -304,7 +310,7 @@ def test_version_print_tag_prints_next_tag( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit.hexsha tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate assert_successful_exit_code(result, cli_cmd) @@ -419,7 +425,7 @@ def test_version_print_tag_prints_next_tag_no_zero_versions( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit.hexsha tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate assert_successful_exit_code(result, cli_cmd) @@ -463,7 +469,7 @@ def test_version_print_last_released_prints_version( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit.hexsha tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate assert_successful_exit_code(result, cli_cmd) @@ -519,7 +525,7 @@ def test_version_print_last_released_prints_released_if_commits( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit.hexsha tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate assert_successful_exit_code(result, cli_cmd) @@ -560,7 +566,7 @@ def test_version_print_last_released_prints_nothing_if_no_tags( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (no release actions should have occurred on print) assert_successful_exit_code(result, cli_cmd) @@ -611,7 +617,7 @@ def test_version_print_last_released_on_detached_head( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (expected -> actual) assert_successful_exit_code(result, cli_cmd) @@ -659,7 +665,7 @@ def test_version_print_last_released_on_nonrelease_branch( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (expected -> actual) assert_successful_exit_code(result, cli_cmd) @@ -714,7 +720,7 @@ def test_version_print_last_released_tag_prints_correct_tag( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit.hexsha tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate assert_successful_exit_code(result, cli_cmd) @@ -779,7 +785,7 @@ def test_version_print_last_released_tag_prints_released_if_commits( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit.hexsha tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate assert_successful_exit_code(result, cli_cmd) @@ -820,7 +826,7 @@ def test_version_print_last_released_tag_prints_nothing_if_no_tags( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (no release actions should have occurred on print) assert_successful_exit_code(result, cli_cmd) @@ -881,7 +887,7 @@ def test_version_print_last_released_tag_on_detached_head( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (expected -> actual) assert_successful_exit_code(result, cli_cmd) @@ -939,7 +945,7 @@ def test_version_print_last_released_tag_on_nonrelease_branch( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (expected -> actual) assert_successful_exit_code(result, cli_cmd) @@ -955,11 +961,12 @@ def test_version_print_last_released_tag_on_nonrelease_branch( @pytest.mark.parametrize( - "repo_result, get_commit_def_fn", + "repo_result, get_commit_def_fn, default_parser", [ ( lazy_fixture(repo_w_trunk_only_conventional_commits.__name__), lazy_fixture(get_commit_def_of_conventional_commit.__name__), + lazy_fixture(default_conventional_parser.__name__), ) ], ) @@ -967,7 +974,8 @@ def test_version_print_next_version_fails_on_detached_head( repo_result: BuiltRepoResult, run_cli: RunCliFn, simulate_change_commits_n_rtn_changelog_entry: SimulateChangeCommitsNReturnChangelogEntryFn, - get_commit_def_fn: GetCommitDefFn, + get_commit_def_fn: GetCommitDefFn[CommitParser[ParseResult, ParserOptions]], + default_parser: CommitParser[ParseResult, ParserOptions], mocked_git_push: MagicMock, post_mocker: Mocker, strip_logging_messages: StripLoggingMessagesFn, @@ -983,7 +991,7 @@ def test_version_print_next_version_fails_on_detached_head( # Setup: make a commit to ensure we have something to release simulate_change_commits_n_rtn_changelog_entry( repo, - [get_commit_def_fn("fix: make a patch fix to codebase")], + [get_commit_def_fn("fix: make a patch fix to codebase", parser=default_parser)], ) # Setup: take measurement before running the version command @@ -999,7 +1007,7 @@ def test_version_print_next_version_fails_on_detached_head( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit.hexsha tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (expected -> actual) assert_exit_code(1, result, cli_cmd) @@ -1015,11 +1023,12 @@ def test_version_print_next_version_fails_on_detached_head( @pytest.mark.parametrize( - "repo_result, get_commit_def_fn", + "repo_result, get_commit_def_fn, default_parser", [ ( lazy_fixture(repo_w_trunk_only_conventional_commits.__name__), lazy_fixture(get_commit_def_of_conventional_commit.__name__), + lazy_fixture(default_conventional_parser.__name__), ) ], ) @@ -1027,7 +1036,8 @@ def test_version_print_next_tag_fails_on_detached_head( repo_result: BuiltRepoResult, run_cli: RunCliFn, simulate_change_commits_n_rtn_changelog_entry: SimulateChangeCommitsNReturnChangelogEntryFn, - get_commit_def_fn: GetCommitDefFn, + get_commit_def_fn: GetCommitDefFn[CommitParser[ParseResult, ParserOptions]], + default_parser: CommitParser[ParseResult, ParserOptions], mocked_git_push: MagicMock, post_mocker: Mocker, strip_logging_messages: StripLoggingMessagesFn, @@ -1043,7 +1053,7 @@ def test_version_print_next_tag_fails_on_detached_head( # Setup: make a commit to ensure we have something to release simulate_change_commits_n_rtn_changelog_entry( repo, - [get_commit_def_fn("fix: make a patch fix to codebase")], + [get_commit_def_fn("fix: make a patch fix to codebase", parser=default_parser)], ) # Setup: take measurement before running the version command @@ -1059,7 +1069,7 @@ def test_version_print_next_tag_fails_on_detached_head( repo_status_after = repo.git.status(short=True) head_after = repo.head.commit.hexsha tags_after = {tag.name for tag in repo.tags} - tags_set_difference = set.difference(tags_after, tags_before) + tags_set_difference = cast("set[str]", set.difference(tags_after, tags_before)) # Evaluate (expected -> actual) assert_exit_code(1, result, cli_cmd) diff --git a/tests/e2e/cmd_version/test_version_stamp.py b/tests/e2e/cmd_version/test_version_stamp.py index 182e9406b..892ce59d7 100644 --- a/tests/e2e/cmd_version/test_version_stamp.py +++ b/tests/e2e/cmd_version/test_version_stamp.py @@ -61,6 +61,7 @@ def test_version_only_stamp_version( post_mocker: MagicMock, example_pyproject_toml: Path, example_project_dir: ExProjectDir, + pyproject_toml_file: Path, ) -> None: repo = repo_result["repo"] version_file = example_project_dir.joinpath( @@ -68,7 +69,7 @@ def test_version_only_stamp_version( ) expected_changed_files = sorted( [ - "pyproject.toml", + str(pyproject_toml_file), str(version_file.relative_to(example_project_dir)), ] ) diff --git a/tests/e2e/test_main.py b/tests/e2e/test_main.py index 45f13d28b..4e1df87e0 100644 --- a/tests/e2e/test_main.py +++ b/tests/e2e/test_main.py @@ -219,10 +219,11 @@ def test_errors_when_config_file_invalid_configuration( run_cli: RunCliFn, update_pyproject_toml: UpdatePyprojectTomlFn, strip_logging_messages: StripLoggingMessagesFn, + pyproject_toml_file: Path, ): # Setup update_pyproject_toml("tool.semantic_release.remote.type", "invalidType") - cli_cmd = [MAIN_PROG_NAME, "--config", "pyproject.toml", VERSION_SUBCMD] + cli_cmd = [MAIN_PROG_NAME, "--config", str(pyproject_toml_file), VERSION_SUBCMD] # Act result = run_cli(cli_cmd[1:]) From 142948cb1f36a959020c7b2882251da41dd43de9 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 1 Sep 2025 20:46:47 -0600 Subject: [PATCH 5/7] test(cmd-changelog): refactor changelog command tests to match new e2e infrastructure --- tests/e2e/cmd_changelog/test_changelog.py | 7 +-- .../test_changelog_custom_parser.py | 10 +++- .../test_changelog_release_notes.py | 47 +++++++------------ 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/tests/e2e/cmd_changelog/test_changelog.py b/tests/e2e/cmd_changelog/test_changelog.py index 3f3bb56da..757e1316b 100644 --- a/tests/e2e/cmd_changelog/test_changelog.py +++ b/tests/e2e/cmd_changelog/test_changelog.py @@ -14,7 +14,6 @@ from semantic_release.changelog.context import ChangelogMode from semantic_release.cli.config import ChangelogOutputFormat from semantic_release.hvcs.github import Github -from semantic_release.version.version import Version from tests.const import ( CHANGELOG_SUBCMD, @@ -77,7 +76,7 @@ from requests_mock import Mocker - from semantic_release.commit_parser.conventional.parser import ( + from semantic_release.commit_parser.conventional import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser @@ -1109,9 +1108,7 @@ def test_custom_release_notes_template( ) -> None: """Verify the template `.release_notes.md.j2` from `template_dir` is used.""" expected_call_count = 1 - version = Version.parse( - get_versions_from_repo_build_def(repo_result["definition"])[-1] - ) + version = get_versions_from_repo_build_def(repo_result["definition"])[-1] # Setup use_release_notes_template() diff --git a/tests/e2e/cmd_changelog/test_changelog_custom_parser.py b/tests/e2e/cmd_changelog/test_changelog_custom_parser.py index 0173cb49d..72b430875 100644 --- a/tests/e2e/cmd_changelog/test_changelog_custom_parser.py +++ b/tests/e2e/cmd_changelog/test_changelog_custom_parser.py @@ -18,6 +18,10 @@ if TYPE_CHECKING: from pathlib import Path + from semantic_release.commit_parser.conventional import ( + ConventionalCommitParser, + ) + from tests.conftest import RunCliFn from tests.fixtures.example_project import UpdatePyprojectTomlFn, UseCustomParserFn from tests.fixtures.git_repo import BuiltRepoResult, GetCommitDefFn @@ -31,9 +35,10 @@ def test_changelog_custom_parser_remove_from_changelog( run_cli: RunCliFn, update_pyproject_toml: UpdatePyprojectTomlFn, use_custom_parser: UseCustomParserFn, - get_commit_def_of_conventional_commit: GetCommitDefFn, + get_commit_def_of_conventional_commit: GetCommitDefFn[ConventionalCommitParser], changelog_md_file: Path, default_md_changelog_insertion_flag: str, + default_conventional_parser: ConventionalCommitParser, ): """ Given when a changelog filtering custom parser is configured @@ -41,7 +46,8 @@ def test_changelog_custom_parser_remove_from_changelog( Then the commit message is not included in the resulting changelog """ ignored_commit_def = get_commit_def_of_conventional_commit( - "chore: do not include me in the changelog" + "chore: do not include me in the changelog", + parser=default_conventional_parser, ) # Because we are in init mode, the insertion flag is not present in the changelog diff --git a/tests/e2e/cmd_changelog/test_changelog_release_notes.py b/tests/e2e/cmd_changelog/test_changelog_release_notes.py index ca6d26563..e125a2f15 100644 --- a/tests/e2e/cmd_changelog/test_changelog_release_notes.py +++ b/tests/e2e/cmd_changelog/test_changelog_release_notes.py @@ -6,8 +6,6 @@ import pytest from pytest_lazy_fixtures import lf as lazy_fixture -from semantic_release.version.version import Version - from tests.const import CHANGELOG_SUBCMD, EXAMPLE_PROJECT_LICENSE, MAIN_PROG_NAME from tests.fixtures.repos import ( repo_w_github_flow_w_default_release_channel_conventional_commits, @@ -21,7 +19,7 @@ if TYPE_CHECKING: from requests_mock import Mocker - from tests.conftest import GetStableDateNowFn, RunCliFn + from tests.conftest import GetCachedRepoDataFn, GetStableDateNowFn, RunCliFn from tests.fixtures.example_project import UpdatePyprojectTomlFn from tests.fixtures.git_repo import ( BuiltRepoResult, @@ -56,19 +54,16 @@ def test_changelog_latest_release_notes( repo_def = repo_result["definition"] tag_format_str: str = get_cfg_value_from_def(repo_def, "tag_format_str") # type: ignore[assignment] repo_actions_per_version = split_repo_actions_by_release_tags( - repo_definition=repo_def, - tag_format_str=tag_format_str, + repo_definition=repo_def ) all_versions = get_versions_from_repo_build_def(repo_def) latest_release_version = all_versions[-1] release_tag = tag_format_str.format(version=latest_release_version) expected_release_notes = generate_default_release_notes_from_def( - version_actions=repo_actions_per_version[release_tag], + version_actions=repo_actions_per_version[latest_release_version], hvcs=get_hvcs_client_from_repo_def(repo_def), - previous_version=( - Version.parse(all_versions[-2]) if len(all_versions) > 1 else None - ), + previous_version=(all_versions[-2] if len(all_versions) > 1 else None), license_name=EXAMPLE_PROJECT_LICENSE, mask_initial_release=get_cfg_value_from_def(repo_def, "mask_initial_release"), ) @@ -130,8 +125,7 @@ def test_changelog_previous_release_notes( repo_def = repo_result["definition"] tag_format_str: str = get_cfg_value_from_def(repo_def, "tag_format_str") # type: ignore[assignment] repo_actions_per_version = split_repo_actions_by_release_tags( - repo_definition=repo_def, - tag_format_str=tag_format_str, + repo_definition=repo_def ) # Extract all versions except for the latest one all_prev_versions = get_versions_from_repo_build_def(repo_def)[:-1] @@ -139,10 +133,10 @@ def test_changelog_previous_release_notes( release_tag = tag_format_str.format(version=latest_release_version) expected_release_notes = generate_default_release_notes_from_def( - version_actions=repo_actions_per_version[release_tag], + version_actions=repo_actions_per_version[latest_release_version], hvcs=get_hvcs_client_from_repo_def(repo_def), previous_version=( - Version.parse(all_prev_versions[-2]) if len(all_prev_versions) > 1 else None + all_prev_versions[-2] if len(all_prev_versions) > 1 else None ), license_name=EXAMPLE_PROJECT_LICENSE, mask_initial_release=mask_initial_release, @@ -170,17 +164,17 @@ def test_changelog_previous_release_notes( @pytest.mark.parametrize( - "repo_result, cache_key, mask_initial_release, license_name", + "repo_result, repo_fixture_name, mask_initial_release, license_name", [ ( lazy_fixture(repo_w_trunk_only_conventional_commits.__name__), - f"psr/repos/{repo_w_trunk_only_conventional_commits.__name__}", + repo_w_trunk_only_conventional_commits.__name__, True, "BSD-3-Clause", ), pytest.param( lazy_fixture(repo_w_trunk_only_conventional_commits.__name__), - f"psr/repos/{repo_w_trunk_only_conventional_commits.__name__}", + repo_w_trunk_only_conventional_commits.__name__, False, "BSD-3-Clause", marks=pytest.mark.comprehensive, @@ -188,7 +182,7 @@ def test_changelog_previous_release_notes( *[ pytest.param( lazy_fixture(repo_fixture_name), - f"psr/repos/{repo_fixture_name}", + repo_fixture_name, mask_initial_release, "BSD-3-Clause", marks=pytest.mark.comprehensive, @@ -216,15 +210,15 @@ def test_changelog_release_notes_license_change( split_repo_actions_by_release_tags: SplitRepoActionsByReleaseTagsFn, generate_default_release_notes_from_def: GenerateDefaultReleaseNotesFromDefFn, update_pyproject_toml: UpdatePyprojectTomlFn, - cache: pytest.Cache, - cache_key: str, + repo_fixture_name: str, stable_now_date: GetStableDateNowFn, + get_cached_repo_data: GetCachedRepoDataFn, ): # Setup repo_def = repo_result["definition"] tag_format_str: str = get_cfg_value_from_def(repo_def, "tag_format_str") # type: ignore[assignment] - if not (repo_build_data := cache.get(cache_key, None)): + if not (repo_build_data := get_cached_repo_data(repo_fixture_name)): pytest.fail("Repo build date not found in cache") repo_build_datetime = datetime.strptime(repo_build_data["build_date"], "%Y-%m-%d") @@ -236,7 +230,6 @@ def test_changelog_release_notes_license_change( repo_actions_per_version = split_repo_actions_by_release_tags( repo_definition=repo_def, - tag_format_str=tag_format_str, ) # Extract all versions all_versions = get_versions_from_repo_build_def(repo_def) @@ -247,21 +240,17 @@ def test_changelog_release_notes_license_change( prev_release_tag = tag_format_str.format(version=previous_release_version) expected_release_notes = generate_default_release_notes_from_def( - version_actions=repo_actions_per_version[latest_release_tag], + version_actions=repo_actions_per_version[latest_release_version], hvcs=get_hvcs_client_from_repo_def(repo_def), - previous_version=( - Version.parse(previous_release_version) if len(all_versions) > 1 else None - ), + previous_version=(previous_release_version if len(all_versions) > 1 else None), license_name=license_name, mask_initial_release=mask_initial_release, ) expected_prev_release_notes = generate_default_release_notes_from_def( - version_actions=repo_actions_per_version[prev_release_tag], + version_actions=repo_actions_per_version[previous_release_version], hvcs=get_hvcs_client_from_repo_def(repo_def), - previous_version=( - Version.parse(all_versions[-3]) if len(all_versions) > 2 else None - ), + previous_version=(all_versions[-3] if len(all_versions) > 2 else None), license_name=EXAMPLE_PROJECT_LICENSE, mask_initial_release=mask_initial_release, ) From 3c2358e6837200affe9e5d97c3da22513ffbeaf1 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Tue, 2 Sep 2025 12:26:18 -0600 Subject: [PATCH 6/7] ci(test-e2e): reduce number of Python versions tested to adhere to SemVer --- .github/workflows/ci.yml | 5 ++++- .github/workflows/cicd.yml | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8658260eb..4576fc45d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,10 @@ jobs: needs: eval-changes uses: ./.github/workflows/validate.yml with: - python-versions-linux: '["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]' + # It was a bit of overkill before testing every minor version, and since this project is all about + # SemVer, we should expect Python to adhere to that model to. Therefore Only test across 2 OS's but + # the lowest supported minor version and the latest stable minor version (just in case). + python-versions-linux: '["3.8", "3.13"]' # Since the test suite takes ~4 minutes to complete on windows, and windows is billed higher # we are only going to run it on the oldest version of python we support. The older version # will be the most likely area to fail as newer minor versions maintain compatibility. diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 01dc69ae6..38d687f50 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -73,8 +73,11 @@ jobs: group: ${{ github.workflow }}-validate-${{ github.ref_name }} cancel-in-progress: true with: - python-versions-linux: '["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]' - python-versions-windows: '["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]' + # It was a bit of overkill before testing every minor version, and since this project is all about + # SemVer, we should expect Python to adhere to that model to. Therefore Only test across 2 OS's but + # the lowest supported minor version and the latest stable minor version. + python-versions-linux: '["3.8", "3.13"]' + python-versions-windows: '["3.8", "3.13"]' files-changed: ${{ needs.eval-changes.outputs.any-file-changes }} build-files-changed: ${{ needs.eval-changes.outputs.build-changes }} ci-files-changed: ${{ needs.eval-changes.outputs.ci-changes }} From e2edcec273a68b57ee2f312bce7425fd475f764b Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Tue, 2 Sep 2025 12:28:44 -0600 Subject: [PATCH 7/7] build(config): lock major version of Python 3 and minimum of v3.8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6aaf43564..ccad3fd6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta" name = "python-semantic-release" version = "10.3.1" description = "Automatic Semantic Versioning for Python projects" -requires-python = ">=3.8" +requires-python = "~= 3.8" license = { text = "MIT" } classifiers = [ "Programming Language :: Python",