From e18f86640a78b374a327848b9e2ba868003d1a43 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 8 Sep 2025 00:37:04 -0600 Subject: [PATCH 1/3] feat(parser): add new conventional-commits standard parser for monorepos (#1143) Resolves: #614 * refactor(parser-conventional): relocate parser file & separate options from parser class file * test(fixtures): refactor e2e infrastructure to support monorepo builds * test(fixtures): add github flow monorepo with squash branch default releases * test(cmd-version): add e2e test of github flow monorepo squashed 1 channel strategy * test(fixtures): add github flow monorepo with feature releases & merge commits * test(cmd-version): add e2e test of github flow monorepo merged 2 channel strategy * test(fixtures): add trunk-only monorepo with official releases only * test(cmd-version): add e2e test of trunk-only, 1 channel monorepo * feat(config): add `conventional-monorepo` as valid `commit_parser` type NOTICE: This release introduces a new built-in parser type that can be utilized for monorepo projects. The type value is `conventional-monorepo` and when specified it will apply the conventional commit parser to a monorepo environment. This parser has specialized options to help handle monorepo projects as well. For more information, please refer to the [Monorepo Docs](https://python-semantic-release.readthedocs.io/en/stable). * docs: add configuration guide for monorepo use with PSR * docs(commit-parsers): introduce conventional commit monorepo parser options & features * docs(configuration): update `commit_parser` option with new `conventional-monorepo` value --- docs/concepts/commit_parsing.rst | 90 +- .../configuration-guides/index.rst | 1 + .../monorepos-ex-easy-before-release.png | Bin 0 -> 116927 bytes .../monorepos-ex-easy-post-release.png | Bin 0 -> 139556 bytes .../configuration-guides/monorepos.rst | 358 +++++++ docs/configuration/configuration.rst | 1 + src/semantic_release/cli/config.py | 6 +- .../commit_parser/__init__.py | 23 + .../commit_parser/conventional/__init__.py | 17 + .../commit_parser/conventional/options.py | 72 ++ .../conventional/options_monorepo.py | 90 ++ .../parser.py} | 212 ++-- .../conventional/parser_monorepo.py | 467 +++++++++ tests/const.py | 3 + tests/e2e/cmd_changelog/test_changelog.py | 2 +- .../test_changelog_custom_parser.py | 2 +- .../e2e/cmd_version/bump_version/conftest.py | 17 +- .../github_flow_monorepo/__init__.py | 0 .../test_monorepo_1_channel.py | 251 +++++ .../test_monorepo_2_channels.py | 250 +++++ .../trunk_based_dev_monorepo/__init__.py | 0 .../test_monorepo_trunk.py | 251 +++++ tests/fixtures/__init__.py | 1 + tests/fixtures/example_project.py | 30 +- tests/fixtures/git_repo.py | 186 +++- tests/fixtures/monorepos/__init__.py | 4 + tests/fixtures/monorepos/example_monorepo.py | 523 ++++++++++ tests/fixtures/monorepos/git_monorepo.py | 206 ++++ .../monorepos/github_flow/__init__.py | 2 + .../github_flow/monorepo_w_default_release.py | 954 ++++++++++++++++++ .../monorepo_w_release_channels.py | 888 ++++++++++++++++ .../monorepos/trunk_based_dev/__init__.py | 1 + .../trunk_based_dev/monorepo_w_tags.py | 623 ++++++++++++ .../git_flow/repo_w_1_release_channel.py | 2 +- .../git_flow/repo_w_2_release_channels.py | 2 +- .../git_flow/repo_w_3_release_channels.py | 2 +- .../git_flow/repo_w_4_release_channels.py | 2 +- .../github_flow/repo_w_default_release.py | 2 +- ...w_default_release_w_branch_update_merge.py | 2 +- .../github_flow/repo_w_release_channels.py | 2 +- tests/fixtures/repos/repo_initial_commit.py | 2 +- .../repo_w_dual_version_support.py | 2 +- ...po_w_dual_version_support_w_prereleases.py | 2 +- .../repos/trunk_based_dev/repo_w_no_tags.py | 2 +- .../trunk_based_dev/repo_w_prereleases.py | 2 +- .../repos/trunk_based_dev/repo_w_tags.py | 2 +- 46 files changed, 5389 insertions(+), 168 deletions(-) create mode 100644 docs/configuration/configuration-guides/monorepos-ex-easy-before-release.png create mode 100644 docs/configuration/configuration-guides/monorepos-ex-easy-post-release.png create mode 100644 docs/configuration/configuration-guides/monorepos.rst create mode 100644 src/semantic_release/commit_parser/conventional/__init__.py create mode 100644 src/semantic_release/commit_parser/conventional/options.py create mode 100644 src/semantic_release/commit_parser/conventional/options_monorepo.py rename src/semantic_release/commit_parser/{conventional.py => conventional/parser.py} (74%) create mode 100644 src/semantic_release/commit_parser/conventional/parser_monorepo.py create mode 100644 tests/e2e/cmd_version/bump_version/github_flow_monorepo/__init__.py create mode 100644 tests/e2e/cmd_version/bump_version/github_flow_monorepo/test_monorepo_1_channel.py create mode 100644 tests/e2e/cmd_version/bump_version/github_flow_monorepo/test_monorepo_2_channels.py create mode 100644 tests/e2e/cmd_version/bump_version/trunk_based_dev_monorepo/__init__.py create mode 100644 tests/e2e/cmd_version/bump_version/trunk_based_dev_monorepo/test_monorepo_trunk.py create mode 100644 tests/fixtures/monorepos/__init__.py create mode 100644 tests/fixtures/monorepos/example_monorepo.py create mode 100644 tests/fixtures/monorepos/git_monorepo.py create mode 100644 tests/fixtures/monorepos/github_flow/__init__.py create mode 100644 tests/fixtures/monorepos/github_flow/monorepo_w_default_release.py create mode 100644 tests/fixtures/monorepos/github_flow/monorepo_w_release_channels.py create mode 100644 tests/fixtures/monorepos/trunk_based_dev/__init__.py create mode 100644 tests/fixtures/monorepos/trunk_based_dev/monorepo_w_tags.py diff --git a/docs/concepts/commit_parsing.rst b/docs/concepts/commit_parsing.rst index 163927c39..c38954b82 100644 --- a/docs/concepts/commit_parsing.rst +++ b/docs/concepts/commit_parsing.rst @@ -49,6 +49,7 @@ Built-in Commit Parsers The following parsers are built in to Python Semantic Release: - :ref:`ConventionalCommitParser ` +- :ref:`ConventionalCommitMonorepoParser ` *(available in ${NEW_RELEASE_TAG}+)* - :ref:`AngularCommitParser ` *(deprecated in v9.19.0)* - :ref:`EmojiCommitParser ` - :ref:`ScipyCommitParser ` @@ -65,7 +66,7 @@ Conventional Commits Parser A parser that is designed to parse commits formatted according to the `Conventional Commits Specification`_. The parser is implemented with the following -logic in relation to how PSR's core features: +logic in relation to PSR's core features: - **Version Bump Determination**: This parser extracts the commit type from the subject line of the commit (the first line of a commit message). This type is matched against @@ -127,13 +128,98 @@ logic in relation to how PSR's core features: If no commit parser options are provided via the configuration, the parser will use PSR's built-in -:py:class:`defaults `. +:py:class:`defaults `. .. _#402: https://github.com/python-semantic-release/python-semantic-release/issues/402 .. _Conventional Commits Specification: https://www.conventionalcommits.org/en/v1.0.0 ---- +.. _commit_parser-builtin-conventional-monorepo: + +Conventional Commits Monorepo Parser +"""""""""""""""""""""""""""""""""""" + +*Introduced in ${NEW_RELEASE_TAG}* + +.. important:: + In order for this parser to be effective, please review the section titled + :ref:`monorepos` for details on file structure, configurations, and release actions. + +This parser is an extension of the :ref:`commit_parser-builtin-conventional`, designed specifically +for monorepo environments. A monorepo environment is defined as a single source control repository +that contains multiple packages, each of which can be released independently and may have different +version numbers. + +This parser introduces two new configuration options that determine which packages are affected +by a commit. These options control whether a commit is considered for version determination, +changelog generation, and other release actions for the relevant packages. The 2 new +configuration options are +:py:class:`path_filters ` +and +:py:class:`scope_prefix `. + +**Features**: + +- **Package Specific Commit Filtering**: For monorepo support, this parser uses 2 filtering rules + to determine if a commit should be considered for a specific package. The first rule is based on + file paths that are changed in the commit and the second rule is based on the optional scope + prefix defined in the commit message. If either rule matches, then the commit is considered + relevant to that package and will be used in version determination, changelog generation, etc, + for that package. If neither rule matches, then the commit is ignored for that package. File + path filtering rules are applied first and are the primary way to determine package relevance. The + :py:class:`path_filters ` + option allows for specifying a list of file path patterns and will also support negated patterns + to ignore specific paths that otherwise would be selected from the file glob pattern. Negated + patterns are defined by prefixing the pattern with an exclamation point (``!``). File path + filtering is quite effective by itself but to handle the edge cases, the parser has the + :py:class:`scope_prefix ` + configuration option to allow the developer to specifically define when the commit is relevant + to the package. In monorepo setups, there are often shared files between packages (generally at + the root project level) that are modified occasionally but not always relevant to the package + being released. Since you do not want to define this path in the package configuration as it may + not be relevant to the release, then this parser will look for a match with the scope prefix. + The scope prefix is a regular expression that is used to match the text inside the scope field + of a Conventional Commit. The scope prefix is optional and is used only if file path filtering + does not match. Commits that have matching files in the commit will be considered relevant to + the package **regardless** if a scope prefix exists or if it matches. + +- **Version Bump Determination**: Once package-specific commit filtering is applied, the relevant + commits are passed to the Conventional Commits Parser for evaluation and then used for version + bump determination. See :ref:`commit_parser-builtin-conventional` for details. + +- **Changelog Generation**: Once package-specific commit filtering is applied, the relevant + commits are passed to the Conventional Commits Parser for evaluation and then used for + changelog generation. See :ref:`commit_parser-builtin-conventional` for details. + +- **Pull/Merge Request Identifier Detection**: Once package-specific commit filtering is applied, + the relevant commits are passed to the Conventional Commits Parser for pull/merge request + identifier detection. See :ref:`commit_parser-builtin-linked_merge_request_detection` for details. + +- **Linked Issue Identifier Detection**: Once package-specific commit filtering is applied, the + relevant commits are passed to the Conventional Commits Parser for linked issue identifier + detection. See :ref:`commit_parser-builtin-issue_number_detection` for details. + +- **Squash Commit Evaluation**: Squashed commits are separated out into individual commits with + the same set of changed files **BEFORE** the package-specific commit filtering is applied. + Each pseudo-commit is then subjected to the same filtering rules as regular commits. See + :ref:`commit_parser-builtin-squash_commit_evaluation` for details. + +- **Release Notice Footer Detection**: Once package-specific commit filtering is applied, the + relevant commits are passed to the Conventional Commits Parser for release notice footer + detection. See :ref:`commit_parser-builtin-release_notice_footer_detection` for details. + +**Limitations**: + +- ``revert`` commit type is NOT supported, see :ref:`commit_parser-builtin-conventional`'s + limitations for details. + +If no commit parser options are provided via the configuration, the parser will use PSR's +built-in +:py:class:`defaults `. + +---- + .. _commit_parser-builtin-angular: Angular Commit Parser diff --git a/docs/configuration/configuration-guides/index.rst b/docs/configuration/configuration-guides/index.rst index 70024dd11..d094496dd 100644 --- a/docs/configuration/configuration-guides/index.rst +++ b/docs/configuration/configuration-guides/index.rst @@ -11,4 +11,5 @@ more specific configurations. .. toctree:: :maxdepth: 1 + Monorepos UV Project Setup diff --git a/docs/configuration/configuration-guides/monorepos-ex-easy-before-release.png b/docs/configuration/configuration-guides/monorepos-ex-easy-before-release.png new file mode 100644 index 0000000000000000000000000000000000000000..6c9bc6113052fad7db2f43ad48dee79f58aca04d GIT binary patch literal 116927 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 Jl6MIc_&>09Q>p*} literal 0 HcmV?d00001 diff --git a/docs/configuration/configuration-guides/monorepos-ex-easy-post-release.png b/docs/configuration/configuration-guides/monorepos-ex-easy-post-release.png new file mode 100644 index 0000000000000000000000000000000000000000..76693ecd3f4f8daacc6f6ec4067a4b4c4ea671d2 GIT binary patch literal 139556 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 Zq# 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

    Kl5a1(39QapvPs1Dy?mxLd@F5N*JlnIV_x* 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 IWy`1k57?B((EtDd literal 0 HcmV?d00001 diff --git a/docs/configuration/configuration-guides/monorepos.rst b/docs/configuration/configuration-guides/monorepos.rst new file mode 100644 index 000000000..63814c5c6 --- /dev/null +++ b/docs/configuration/configuration-guides/monorepos.rst @@ -0,0 +1,358 @@ +.. _monorepos: + +Releasing Packages from a Monorepo +================================== + + +A monorepo (mono-repository) is a software development strategy where code for multiple projects is stored in a single source control system. This approach streamlines and consolidates configuration, but introduces complexities when using automated tools like Python Semantic Release (PSR). + +Previously, PSR offered limited compatibility with monorepos. As of ${NEW_RELEASE_TAG}, PSR introduces the :ref:`commit_parser-builtin-conventional-monorepo`, designed specifically for monorepo environments. To fully leverage this new parser, you must configure your monorepo as described below. + +.. _monorepos-config: + +Configuring PSR +--------------- + +.. _monorepos-config-example_simple: + +Example: Simple +""""""""""""""" + + +**Directory Structure**: PSR does not yet support a single, workspace-level configuration definition. This means each package in the monorepo requires its own PSR configuration file. A compatible and common monorepo file structure looks like: + +.. code:: + + project/ + ├── .git/ + ├── .venv/ + ├── packages/ + │ ├── pkg1/ + │ │ ├── docs/ + │ │ │ └── source/ + │ │ │ ├── conf.py + │ │ │ └── index.rst + │ │ │ + │ │ ├── src/ + │ │ │ └── pkg1/ + │ │ │ ├── __init__.py + │ │ │ ├── __main__.py + │ │ │ └── py.typed + │ │ │ + │ │ ├── CHANGELOG.md + │ │ ├── README.md + │ │ └── pyproject.toml <-- PSR Configuration for Package 1 + │ │ + │ └── pkg2/ + │ ├── docs/ + │ │ └── source/ + │ │ ├── conf.py + │ │ └── index.rst + │ ├── src/ + │ │ └── pkg2/ + │ │ ├── __init__.py + │ │ ├── __main__.py + │ │ └── py.typed + │ │ + │ ├── CHANGELOG.md + │ ├── README.md + │ └── pyproject.toml <-- PSR Configuration for Package 2 + │ + ├── .gitignore + └── README.md + + +This is the most basic monorepo structure, where each package is self-contained with its own configuration files, documentation, and CHANGELOG files. To release a package, change your current working directory to the package directory and execute PSR's :ref:`cmd-version`. PSR will automatically read the package's ``pyproject.toml``, looking for the ``[tool.semantic_release]`` section to determine the package's versioning and release configuration, then search up the file tree to find the Git repository. + +Because there is no workspace-level configuration, you must duplicate any common PSR configuration in each package's configuration file. Customize each configuration for each package to specify how PSR should distinguish between commits. + +With the example file structure above, here is an example configuration file for each package: + +.. code-block:: toml + + # FILE: pkg1/pyproject.toml + [project] + name = "pkg1" + version = "1.0.0" + + [tool.semantic_release] + commit_parser = "conventional-monorepo" + commit_message = """\ + chore(release): pkg1@{version}` + + Automatically generated by python-semantic-release + """ + tag_format = "pkg1-v{version}" + version_toml = ["pyproject.toml:project.version"] + + [tool.semantic_release.commit_parser_options] + path_filters = ["."] + scope_prefix = "pkg1-" + +.. code-block:: toml + + # FILE: pkg2/pyproject.toml + [project] + name = "pkg2" + version = "1.0.0" + + [tool.semantic_release] + commit_parser = "conventional-monorepo" + commit_message = """\ + chore(release): pkg2@{version}` + + Automatically generated by python-semantic-release + """ + tag_format = "pkg2-v{version}" + version_toml = ["pyproject.toml:project.version"] + + [tool.semantic_release.commit_parser_options] + path_filters = ["."] + scope_prefix = "pkg2-" + + +These are the minimum configuration options required for each package. Note the use of :ref:`config-tag_format` to distinguish tags between packages. The commit parser options are specific to the new :ref:`commit_parser-builtin-conventional-monorepo` and play a significant role in identifying which commits are relevant to each package. Since you are expected to change directories to each package before releasing, file paths in each configuration file should be relative to the package directory. + +Each package also defines a slightly different :ref:`config-commit_message` to reflect the package name in each message. This helps clarify which release number is being updated in the commit history. + + +Release Steps +''''''''''''' + +Given the following Git history of a monorepo using a GitHub Flow branching strategy (without CI/CD): + +.. image:: ./monorepos-ex-easy-before-release.png + +To manually release both packages, run: + +.. code-block:: bash + + cd packages/pkg1 + semantic-release version + # 1.0.1 (tag: pkg1-v1.0.1) + + cd ../pkg2 + semantic-release version + # 1.1.0 (tag: pkg2-v1.1.0) + +After releasing both packages, the resulting Git history will look like: + +.. image:: ./monorepos-ex-easy-post-release.png + +.. seealso:: + + - :ref:`GitHub Actions with Monorepos ` + + +Considerations +'''''''''''''' + +1. **Custom Changelogs**: Managing changelogs can be tricky depending on where you want to write the changelog files. In this simple example, the changelog is located within each package directory, and the changelog template does not have any package-specific formatting or naming convention. You can use one shared template directory at the root of the project and configure each package to point to the shared template directory. + +.. code-block:: toml + + # FILE: pkg1/pyproject.toml + [tool.semantic_release] + template_dir = "../../config/release-templates" + +.. code-block:: toml + + # FILE: pkg2/pyproject.toml + [tool.semantic_release] + template_dir = "../../config/release-templates" + +.. code:: + + project/ + ├── .git/ + ├── config/ + │ └── release-templates/ + │ ├── CHANGELOG.md.j2 + │ └── .release_notes.md.j2 + ├── packages/ + │ ├── pkg1/ + │ │ ├── CHANGELOG.md + │ │ └── pyproject.toml + │ │ + │ └── pkg2/ + │ ├── CHANGELOG.md + │ └── pyproject.toml + │ + ├── .gitignore + └── README.md + +.. seealso:: + + - For situations with more complex documentation needs, see our :ref:`Advanced Example `. + + +2. **Package Prereleases**: Creating pre-releases is possible, but it is recommended to use package-prefixed branch names to avoid collisions between packages. For example, to enable alpha pre-releases for new features in both packages, use the following configuration: + +.. code-block:: toml + + # FILE: pkg1/pyproject.toml + [tool.semantic_release.branches.alpha-release] + match = "^pkg1/feat/.+" # <-- note pkg1 prefix + prerelease = true + prerelease_token = "alpha" + +.. code-block:: toml + + # FILE: pkg2/pyproject.toml + [tool.semantic_release.branches.alpha-release] + match = "^pkg2/feat/.+" # <-- note pkg2 prefix + prerelease = true + prerelease_token = "alpha" + +---- + +.. _monorepos-config-example_advanced: + +Example: Advanced +""""""""""""""""" + + +If you want to consolidate documentation into a single top-level directory, the setup becomes more complex. In this example, there is a common documentation folder at the top level, and each package has its own subfolder within the documentation folder. + +Due to naming conventions, PSR cannot automatically accomplish this with its default changelog templates. For this scenario, you must copy the internal PSR templates into a custom directory (even if you do not modify them) and add custom scripting to prepare for each release. + +The directory structure looks like: + +.. code:: + + project/ + ├── .git/ + ├── docs/ + │ ├── source/ + │ │ ├── pkg1/ + │ │ │ ├── changelog.md + │ │ │ └── README.md + │ │ ├── pkg2/ + │ │ │ ├── changelog.md + │ │ │ └── README.md + │ │ └── index.rst + │ │ + │ └── templates/ + │ ├── .base_changelog_template/ + │ │ ├── components/ + │ │ │ ├── changelog_header.md.j2 + │ │ │ ├── changelog_init.md.j2 + │ │ │ ├── changelog_update.md.j2 + │ │ │ ├── changes.md.j2 + │ │ │ ├── first_release.md.j2 + │ │ │ ├── macros.md.j2 + │ │ │ ├── unreleased_changes.md.j2 + │ │ │ └── versioned_changes.md.j2 + │ │ └── changelog.md.j2 + │ ├── .gitignore + │ └── .release_notes.md.j2 + │ + ├── packages/ + │ ├── pkg1/ + │ │ ├── src/ + │ │ │ └── pkg1/ + │ │ │ ├── __init__.py + │ │ │ └── __main__.py + │ │ └── pyproject.toml + │ │ + │ └── pkg2/ + │ ├── src/ + │ │ └── pkg2/ + │ │ ├── __init__.py + │ │ └── __main__.py + │ └── pyproject.toml + │ + └── scripts/ + ├── release-pkg1.sh + └── release-pkg2.sh + + +Each package should point to the ``docs/templates/`` directory to use a common release notes template. PSR ignores hidden files and directories when searching for template files to create, allowing you to hide shared templates in the directory for use in your release setup script. + +Here is our configuration file for package 1 (package 2 is similarly defined): + +.. code-block:: toml + + # FILE: pkg1/pyproject.toml + [project] + name = "pkg1" + version = "1.0.0" + + [tool.semantic_release] + commit_parser = "conventional-monorepo" + commit_message = """\ + chore(release): Release `pkg1@{version}` + + Automatically generated by python-semantic-release + """ + tag_format = "pkg1-v{version}" + version_toml = ["pyproject.toml:project.version"] + + [tool.semantic_release.commit_parser_options] + path_filters = [ + ".", + "../../../docs/source/pkg1/**", + ] + scope_prefix = "pkg1-" + + [tool.semantic_release.changelog] + template_dir = "../../../docs/templates" + mode = "update" + exclude_commit_patterns = [ + '''^chore(?:\([^)]*?\))?: .+''', + '''^ci(?:\([^)]*?\))?: .+''', + '''^refactor(?:\([^)]*?\))?: .+''', + '''^style(?:\([^)]*?\))?: .+''', + '''^test(?:\([^)]*?\))?: .+''', + '''^Initial [Cc]ommit''', + ] + + [tool.semantic_release.changelog.default_templates] + # To enable update mode: this value must set here because the default is not the + # same as the default in the other package & must be the final destination filename + # for the changelog relative to this file + changelog_file = "../../../docs/source/pkg1/changelog.md" + + +Note: In this configuration, we added path filters for additional documentation files related to the package so that the changelog will include documentation changes as well. + + +Next, define a release script to set up the common changelog templates in the correct directory format so PSR will create the desired files at the proper locations. Following the :ref:`changelog-templates-template-rendering` reference, you must define the folder structure from the root of the project within the templates directory so PSR will properly lay down the files across the repository. The script cleans up any previous templates, dynamically creates the necessary directories, and copies over the shared templates into a package-named directory. Now you are prepared to run PSR for a release of ``pkg1``. + +.. code-block:: bash + + #!/bin/bash + # FILE: scripts/release-pkg1.sh + + set -euo pipefail + + PROJECT_ROOT="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")")" + VIRTUAL_ENV="$PROJECT_ROOT/.venv" + + PACKAGE_NAME="pkg1" + + cd "$PROJECT_ROOT" || exit 1 + + # Setup documentation template + pushd "docs/templates" >/dev/null || exit 1 + + rm -rf docs/ + mkdir -p "docs/source/" + cp -r .base_changelog_template/ "docs/source/$PACKAGE_NAME" + + popd >/dev/null || exit 1 + + # Release the package + pushd "packages/$PACKAGE_NAME" >/dev/null || exit 1 + + printf '%s\n' "Releasing $PACKAGE_NAME..." + "$VIRTUAL_ENV/bin/semantic-release" -v version --no-push + + popd >/dev/null || exit 1 + + +That's it! This example demonstrates how to set up a monorepo with shared changelog templates and a consolidated documentation folder for multiple packages. + +.. seealso:: + + - Advanced Example Monorepo: `codejedi365/psr-monorepo-poweralpha `_ diff --git a/docs/configuration/configuration.rst b/docs/configuration/configuration.rst index d9c154a93..6c5abd376 100644 --- a/docs/configuration/configuration.rst +++ b/docs/configuration/configuration.rst @@ -796,6 +796,7 @@ within the Git repository. Built-in parsers: * ``angular`` - :ref:`AngularCommitParser ` *(deprecated in v9.19.0)* * ``conventional`` - :ref:`ConventionalCommitParser ` *(available in v9.19.0+)* + * ``conventional-monorepo`` - :ref:`ConventionalCommitMonorepoParser ` *(available in ${NEW_RELEASE_TAG}+)* * ``emoji`` - :ref:`EmojiCommitParser ` * ``scipy`` - :ref:`ScipyCommitParser ` * ``tag`` - :ref:`TagCommitParser ` *(deprecated in v9.12.0)* diff --git a/src/semantic_release/cli/config.py b/src/semantic_release/cli/config.py index 1d2057a48..37b86a811 100644 --- a/src/semantic_release/cli/config.py +++ b/src/semantic_release/cli/config.py @@ -39,6 +39,7 @@ from semantic_release.commit_parser import ( AngularCommitParser, CommitParser, + ConventionalCommitMonorepoParser, ConventionalCommitParser, EmojiCommitParser, ParseResult, @@ -71,9 +72,10 @@ class HvcsClient(str, Enum): GITEA = "gitea" -_known_commit_parsers: Dict[str, type[CommitParser]] = { - "conventional": ConventionalCommitParser, +_known_commit_parsers: dict[str, type[CommitParser[Any, Any]]] = { "angular": AngularCommitParser, + "conventional": ConventionalCommitParser, + "conventional-monorepo": ConventionalCommitMonorepoParser, "emoji": EmojiCommitParser, "scipy": ScipyCommitParser, "tag": TagCommitParser, diff --git a/src/semantic_release/commit_parser/__init__.py b/src/semantic_release/commit_parser/__init__.py index 740f4ae7f..15a96c176 100644 --- a/src/semantic_release/commit_parser/__init__.py +++ b/src/semantic_release/commit_parser/__init__.py @@ -7,6 +7,8 @@ AngularParserOptions, ) from semantic_release.commit_parser.conventional import ( + ConventionalCommitMonorepoParser, + ConventionalCommitMonorepoParserOptions, ConventionalCommitParser, ConventionalCommitParserOptions, ) @@ -28,3 +30,24 @@ ParseResult, ParseResultType, ) + +__all__ = [ + "CommitParser", + "ParserOptions", + "AngularCommitParser", + "AngularParserOptions", + "ConventionalCommitParser", + "ConventionalCommitParserOptions", + "ConventionalCommitMonorepoParser", + "ConventionalCommitMonorepoParserOptions", + "EmojiCommitParser", + "EmojiParserOptions", + "ScipyCommitParser", + "ScipyParserOptions", + "TagCommitParser", + "TagParserOptions", + "ParsedCommit", + "ParseError", + "ParseResult", + "ParseResultType", +] diff --git a/src/semantic_release/commit_parser/conventional/__init__.py b/src/semantic_release/commit_parser/conventional/__init__.py new file mode 100644 index 000000000..dd7d57d63 --- /dev/null +++ b/src/semantic_release/commit_parser/conventional/__init__.py @@ -0,0 +1,17 @@ +from semantic_release.commit_parser.conventional.options import ( + ConventionalCommitParserOptions, +) +from semantic_release.commit_parser.conventional.options_monorepo import ( + ConventionalCommitMonorepoParserOptions, +) +from semantic_release.commit_parser.conventional.parser import ConventionalCommitParser +from semantic_release.commit_parser.conventional.parser_monorepo import ( + ConventionalCommitMonorepoParser, +) + +__all__ = [ + "ConventionalCommitParser", + "ConventionalCommitParserOptions", + "ConventionalCommitMonorepoParser", + "ConventionalCommitMonorepoParserOptions", +] diff --git a/src/semantic_release/commit_parser/conventional/options.py b/src/semantic_release/commit_parser/conventional/options.py new file mode 100644 index 000000000..6bdb9739c --- /dev/null +++ b/src/semantic_release/commit_parser/conventional/options.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from itertools import zip_longest +from typing import Tuple + +from pydantic.dataclasses import dataclass + +from semantic_release.commit_parser._base import ParserOptions +from semantic_release.enums import LevelBump + + +@dataclass +class ConventionalCommitParserOptions(ParserOptions): + """Options dataclass for the ConventionalCommitParser.""" + + minor_tags: Tuple[str, ...] = ("feat",) + """Commit-type prefixes that should result in a minor release bump.""" + + patch_tags: Tuple[str, ...] = ("fix", "perf") + """Commit-type prefixes that should result in a patch release bump.""" + + other_allowed_tags: Tuple[str, ...] = ( + "build", + "chore", + "ci", + "docs", + "style", + "refactor", + "test", + ) + """Commit-type prefixes that are allowed but do not result in a version bump.""" + + allowed_tags: Tuple[str, ...] = ( + *minor_tags, + *patch_tags, + *other_allowed_tags, + ) + """ + All commit-type prefixes that are allowed. + + These are used to identify a valid commit message. If a commit message does not start with + one of these prefixes, it will not be considered a valid commit message. + """ + + default_bump_level: LevelBump = LevelBump.NO_RELEASE + """The minimum bump level to apply to valid commit message.""" + + parse_squash_commits: bool = True + """Toggle flag for whether or not to parse squash commits""" + + ignore_merge_commits: bool = True + """Toggle flag for whether or not to ignore merge commits""" + + @property + def tag_to_level(self) -> dict[str, LevelBump]: + """A mapping of commit tags to the level bump they should result in.""" + return self._tag_to_level + + def __post_init__(self) -> None: + self._tag_to_level: dict[str, LevelBump] = { + str(tag): level + for tag, level in [ + # we have to do a type ignore as zip_longest provides a type that is not specific enough + # for our expected output. Due to the empty second array, we know the first is always longest + # and that means no values in the first entry of the tuples will ever be a LevelBump. We + # apply a str() to make mypy happy although it will never happen. + *zip_longest(self.allowed_tags, (), fillvalue=self.default_bump_level), + *zip_longest(self.patch_tags, (), fillvalue=LevelBump.PATCH), + *zip_longest(self.minor_tags, (), fillvalue=LevelBump.MINOR), + ] + if "|" not in str(tag) + } diff --git a/src/semantic_release/commit_parser/conventional/options_monorepo.py b/src/semantic_release/commit_parser/conventional/options_monorepo.py new file mode 100644 index 000000000..58bcaf47d --- /dev/null +++ b/src/semantic_release/commit_parser/conventional/options_monorepo.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from pathlib import Path +from re import compile as regexp, error as RegExpError # noqa: N812 +from typing import TYPE_CHECKING, Any, Iterable, Tuple + +from pydantic import Field, field_validator +from pydantic.dataclasses import dataclass + +# typing_extensions is for Python 3.8, 3.9, 3.10 compatibility +from typing_extensions import Annotated + +from semantic_release.commit_parser.conventional.options import ( + ConventionalCommitParserOptions, +) + +if TYPE_CHECKING: # pragma: no cover + pass + + +@dataclass +class ConventionalCommitMonorepoParserOptions(ConventionalCommitParserOptions): + # TODO: add example into the docstring + """Options dataclass for ConventionalCommitMonorepoParser.""" + + path_filters: Annotated[Tuple[str, ...], Field(validate_default=True)] = (".",) + """ + A set of relative paths to filter commits by. Only commits with file changes that + match these file paths or its subdirectories will be considered valid commits. + + Syntax is similar to .gitignore with file path globs and inverse file match globs + via `!` prefix. Paths should be relative to the current working directory. + """ + + scope_prefix: str = "" + """ + A prefix that will be striped from the scope when parsing commit messages. + + If set, it will cause unscoped commits to be ignored. Use this in tandem with + the `path_filters` option to filter commits by directory and scope. This will + be fed into a regular expression so you must escape any special characters that + are meaningful in regular expressions (e.g. `.`, `*`, `?`, `+`, etc.) if you want + to match them literally. + """ + + @classmethod + @field_validator("path_filters", mode="before") + def convert_strs_to_paths(cls, value: Any) -> tuple[Path, ...]: + values = value if isinstance(value, Iterable) else [value] + results: list[Path] = [] + + for val in values: + if isinstance(val, (str, Path)): + results.append(Path(val)) + continue + + raise TypeError(f"Invalid type: {type(val)}, expected str or Path.") + + return tuple(results) + + @classmethod + @field_validator("path_filters", mode="after") + def resolve_path(cls, dir_paths: tuple[Path, ...]) -> tuple[Path, ...]: + return tuple( + ( + Path(f"!{Path(str_path[1:]).expanduser().absolute().resolve()}") + # maintains the negation prefix if it exists + if (str_path := str(path)).startswith("!") + # otherwise, resolve the path normally + else path.expanduser().absolute().resolve() + ) + for path in dir_paths + ) + + @classmethod + @field_validator("scope_prefix", mode="after") + def validate_scope_prefix(cls, scope_prefix: str) -> str: + if not scope_prefix: + return "" + + # Allow the special case of a plain wildcard although it's not a valid regex + if scope_prefix == "*": + return ".*" + + try: + regexp(scope_prefix) + except RegExpError as err: + raise ValueError(f"Invalid regex {scope_prefix!r}") from err + + return scope_prefix diff --git a/src/semantic_release/commit_parser/conventional.py b/src/semantic_release/commit_parser/conventional/parser.py similarity index 74% rename from src/semantic_release/commit_parser/conventional.py rename to src/semantic_release/commit_parser/conventional/parser.py index 3cd50d9c7..5cab34c56 100644 --- a/src/semantic_release/commit_parser/conventional.py +++ b/src/semantic_release/commit_parser/conventional/parser.py @@ -1,16 +1,25 @@ from __future__ import annotations -import re from functools import reduce -from itertools import zip_longest -from re import compile as regexp +from logging import getLogger +from re import ( + DOTALL, + IGNORECASE, + MULTILINE, + Match as RegexMatch, + Pattern, + compile as regexp, + error as RegexError, # noqa: N812 +) from textwrap import dedent -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING, ClassVar from git.objects.commit import Commit -from pydantic.dataclasses import dataclass -from semantic_release.commit_parser._base import CommitParser, ParserOptions +from semantic_release.commit_parser._base import CommitParser +from semantic_release.commit_parser.conventional.options import ( + ConventionalCommitParserOptions, +) from semantic_release.commit_parser.token import ( ParsedCommit, ParsedMessageResult, @@ -25,16 +34,10 @@ ) from semantic_release.enums import LevelBump from semantic_release.errors import InvalidParserOptions -from semantic_release.globals import logger from semantic_release.helpers import sort_numerically, text_reducer -if TYPE_CHECKING: # pragma: no cover - from git.objects.commit import Commit - - -def _logged_parse_error(commit: Commit, error: str) -> ParseError: - logger.debug(error) - return ParseError(commit, error=error) +if TYPE_CHECKING: + pass # TODO: Remove from here, allow for user customization instead via options @@ -53,69 +56,6 @@ def _logged_parse_error(commit: Commit, error: str) -> ParseError: } -@dataclass -class ConventionalCommitParserOptions(ParserOptions): - """Options dataclass for the ConventionalCommitParser.""" - - minor_tags: Tuple[str, ...] = ("feat",) - """Commit-type prefixes that should result in a minor release bump.""" - - patch_tags: Tuple[str, ...] = ("fix", "perf") - """Commit-type prefixes that should result in a patch release bump.""" - - other_allowed_tags: Tuple[str, ...] = ( - "build", - "chore", - "ci", - "docs", - "style", - "refactor", - "test", - ) - """Commit-type prefixes that are allowed but do not result in a version bump.""" - - allowed_tags: Tuple[str, ...] = ( - *minor_tags, - *patch_tags, - *other_allowed_tags, - ) - """ - All commit-type prefixes that are allowed. - - These are used to identify a valid commit message. If a commit message does not start with - one of these prefixes, it will not be considered a valid commit message. - """ - - default_bump_level: LevelBump = LevelBump.NO_RELEASE - """The minimum bump level to apply to valid commit message.""" - - parse_squash_commits: bool = True - """Toggle flag for whether or not to parse squash commits""" - - ignore_merge_commits: bool = True - """Toggle flag for whether or not to ignore merge commits""" - - @property - def tag_to_level(self) -> dict[str, LevelBump]: - """A mapping of commit tags to the level bump they should result in.""" - return self._tag_to_level - - def __post_init__(self) -> None: - self._tag_to_level: dict[str, LevelBump] = { - str(tag): level - for tag, level in [ - # we have to do a type ignore as zip_longest provides a type that is not specific enough - # for our expected output. Due to the empty second array, we know the first is always longest - # and that means no values in the first entry of the tuples will ever be a LevelBump. We - # apply a str() to make mypy happy although it will never happen. - *zip_longest(self.allowed_tags, (), fillvalue=self.default_bump_level), - *zip_longest(self.patch_tags, (), fillvalue=LevelBump.PATCH), - *zip_longest(self.minor_tags, (), fillvalue=LevelBump.MINOR), - ] - if "|" not in str(tag) - } - - class ConventionalCommitParser( CommitParser[ParseResult, ConventionalCommitParserOptions] ): @@ -128,14 +68,57 @@ class ConventionalCommitParser( # TODO: Deprecate in lieu of get_default_options() parser_options = ConventionalCommitParserOptions + # GitHub & Gitea use (#123), GitLab uses (!123), and BitBucket uses (pull request #123) + mr_selector = regexp(r"[\t ]+\((?:pull request )?(?P[#!]\d+)\)[\t ]*$") + + issue_selector = regexp( + str.join( + "", + [ + r"^(?:clos(?:e|es|ed|ing)|fix(?:es|ed|ing)?|resolv(?:e|es|ed|ing)|implement(?:s|ed|ing)?):", + r"[\t ]+(?P.+)[\t ]*$", + ], + ), + flags=MULTILINE | IGNORECASE, + ) + + notice_selector = regexp(r"^NOTICE: (?P.+)$") + + common_commit_msg_filters: ClassVar[dict[str, tuple[Pattern[str], str]]] = { + "typo-extra-spaces": (regexp(r"(\S) +(\S)"), r"\1 \2"), + "git-header-commit": ( + regexp(r"^[\t ]*commit [0-9a-f]+$\n?", flags=MULTILINE), + "", + ), + "git-header-author": ( + regexp(r"^[\t ]*Author: .+$\n?", flags=MULTILINE), + "", + ), + "git-header-date": ( + regexp(r"^[\t ]*Date: .+$\n?", flags=MULTILINE), + "", + ), + "git-squash-heading": ( + regexp( + r"^[\t ]*Squashed commit of the following:.*$\n?", + flags=MULTILINE, + ), + "", + ), + } + def __init__(self, options: ConventionalCommitParserOptions | None = None) -> None: super().__init__(options) + self._logger = getLogger( + str.join(".", [self.__module__, self.__class__.__name__]) + ) + try: commit_type_pattern = regexp( r"(?P%s)" % str.join("|", self.options.allowed_tags) ) - except re.error as err: + except RegexError as err: raise InvalidParserOptions( str.join( "\n", @@ -167,45 +150,11 @@ def __init__(self, options: ConventionalCommitParserOptions | None = None) -> No r"(?:\n\n(?P.+))?", # commit body ], ), - flags=re.DOTALL, + flags=DOTALL, ) - # GitHub & Gitea use (#123), GitLab uses (!123), and BitBucket uses (pull request #123) - self.mr_selector = regexp( - r"[\t ]+\((?:pull request )?(?P[#!]\d+)\)[\t ]*$" - ) - self.issue_selector = regexp( - str.join( - "", - [ - r"^(?:clos(?:e|es|ed|ing)|fix(?:es|ed|ing)?|resolv(?:e|es|ed|ing)|implement(?:s|ed|ing)?):", - r"[\t ]+(?P.+)[\t ]*$", - ], - ), - flags=re.MULTILINE | re.IGNORECASE, - ) - self.notice_selector = regexp(r"^NOTICE: (?P.+)$") - self.filters = { - "typo-extra-spaces": (regexp(r"(\S) +(\S)"), r"\1 \2"), - "git-header-commit": ( - regexp(r"^[\t ]*commit [0-9a-f]+$\n?", flags=re.MULTILINE), - "", - ), - "git-header-author": ( - regexp(r"^[\t ]*Author: .+$\n?", flags=re.MULTILINE), - "", - ), - "git-header-date": ( - regexp(r"^[\t ]*Date: .+$\n?", flags=re.MULTILINE), - "", - ), - "git-squash-heading": ( - regexp( - r"^[\t ]*Squashed commit of the following:.*$\n?", - flags=re.MULTILINE, - ), - "", - ), + self.filters: dict[str, tuple[Pattern[str], str]] = { + **self.common_commit_msg_filters, "git-squash-commit-prefix": ( regexp( str.join( @@ -215,17 +164,20 @@ def __init__(self, options: ConventionalCommitParserOptions | None = None) -> No commit_type_pattern.pattern + r"\b", # prior to commit type ], ), - flags=re.MULTILINE, + flags=MULTILINE, ), # move commit type to the start of the line r"\1", ), } - @staticmethod - def get_default_options() -> ConventionalCommitParserOptions: + def get_default_options(self) -> ConventionalCommitParserOptions: return ConventionalCommitParserOptions() + def log_parse_error(self, commit: Commit, error: str) -> ParseError: + self._logger.debug(error) + return ParseError(commit, error=error) + def commit_body_components_separator( self, accumulator: dict[str, list[str]], text: str ) -> dict[str, list[str]]: @@ -267,14 +219,20 @@ def commit_body_components_separator( return accumulator def parse_message(self, message: str) -> ParsedMessageResult | None: - if not (parsed := self.commit_msg_pattern.match(message)): - return None + return ( + self.create_parsed_message_result(match) + if (match := self.commit_msg_pattern.match(message)) + else None + ) - parsed_break = parsed.group("break") - parsed_scope = parsed.group("scope") or "" - parsed_subject = parsed.group("subject") - parsed_text = parsed.group("text") - parsed_type = parsed.group("type") + def create_parsed_message_result( + self, match: RegexMatch[str] + ) -> ParsedMessageResult: + parsed_break = match.group("break") + parsed_scope = match.group("scope") or "" + parsed_subject = match.group("subject") + parsed_text = match.group("text") + parsed_type = match.group("type") linked_merge_request = "" if mr_match := self.mr_selector.search(parsed_subject): @@ -322,7 +280,7 @@ def is_merge_commit(commit: Commit) -> bool: def parse_commit(self, commit: Commit) -> ParseResult: if not (parsed_msg_result := self.parse_message(force_str(commit.message))): - return _logged_parse_error( + return self.log_parse_error( commit, f"Unable to parse commit message: {commit.message!r}", ) @@ -342,7 +300,7 @@ def parse(self, commit: Commit) -> ParseResult | list[ParseResult]: will be returned as a list of a single ParseResult. """ if self.options.ignore_merge_commits and self.is_merge_commit(commit): - return _logged_parse_error( + return self.log_parse_error( commit, "Ignoring merge commit: %s" % commit.hexsha[:8] ) diff --git a/src/semantic_release/commit_parser/conventional/parser_monorepo.py b/src/semantic_release/commit_parser/conventional/parser_monorepo.py new file mode 100644 index 000000000..18a938c43 --- /dev/null +++ b/src/semantic_release/commit_parser/conventional/parser_monorepo.py @@ -0,0 +1,467 @@ +from __future__ import annotations + +import os +from fnmatch import fnmatch +from logging import getLogger +from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath +from re import DOTALL, compile as regexp, error as RegexError # noqa: N812 +from typing import TYPE_CHECKING + +from semantic_release.commit_parser._base import CommitParser +from semantic_release.commit_parser.conventional.options import ( + ConventionalCommitParserOptions, +) +from semantic_release.commit_parser.conventional.options_monorepo import ( + ConventionalCommitMonorepoParserOptions, +) +from semantic_release.commit_parser.conventional.parser import ConventionalCommitParser +from semantic_release.commit_parser.token import ( + ParsedCommit, + ParsedMessageResult, + ParseError, + ParseResult, +) +from semantic_release.commit_parser.util import force_str +from semantic_release.errors import InvalidParserOptions + +if TYPE_CHECKING: # pragma: no cover + from git.objects.commit import Commit + + +class ConventionalCommitMonorepoParser( + CommitParser[ParseResult, ConventionalCommitMonorepoParserOptions] +): + # TODO: Remove for v11 compatibility, get_default_options() will be called instead + parser_options = ConventionalCommitMonorepoParserOptions + + def __init__( + self, options: ConventionalCommitMonorepoParserOptions | None = None + ) -> None: + super().__init__(options) + + try: + commit_scope_pattern = regexp( + r"\(" + self.options.scope_prefix + r"(?P[^\n]+)?\)", + ) + except RegexError as err: + raise InvalidParserOptions( + str.join( + "\n", + [ + f"Invalid options for {self.__class__.__name__}", + "Unable to create regular expression from configured scope_prefix.", + "Please check the configured scope_prefix and remove or escape any regular expression characters.", + ], + ) + ) from err + + try: + commit_type_pattern = regexp( + r"(?P%s)" % str.join("|", self.options.allowed_tags) + ) + except RegexError as err: + raise InvalidParserOptions( + str.join( + "\n", + [ + f"Invalid options for {self.__class__.__name__}", + "Unable to create regular expression from configured commit-types.", + "Please check the configured commit-types and remove or escape any regular expression characters.", + ], + ) + ) from err + + # This regular expression includes scope prefix into the pattern and forces a scope to be present + # PSR will match the full scope but we don't include it in the scope match, + # which implicitly strips it from being included in the returned scope. + self._strict_scope_pattern = regexp( + str.join( + "", + [ + r"^" + commit_type_pattern.pattern, + commit_scope_pattern.pattern, + r"(?P!)?:\s+", + r"(?P[^\n]+)", + r"(?:\n\n(?P.+))?", # commit body + ], + ), + flags=DOTALL, + ) + + self._optional_scope_pattern = regexp( + str.join( + "", + [ + r"^" + commit_type_pattern.pattern, + r"(?:\((?P[^\n]+)\))?", + r"(?P!)?:\s+", + r"(?P[^\n]+)", + r"(?:\n\n(?P.+))?", # commit body + ], + ), + flags=DOTALL, + ) + + file_select_filters, file_ignore_filters = self._process_path_filter_options( + self.options.path_filters + ) + self._file_selection_filters: list[str] = file_select_filters + self._file_ignore_filters: list[str] = file_ignore_filters + + self._logger = getLogger( + str.join(".", [self.__module__, self.__class__.__name__]) + ) + + self._base_parser = ConventionalCommitParser( + options=ConventionalCommitParserOptions( + **{ + k: getattr(self.options, k) + for k in ConventionalCommitParserOptions().__dataclass_fields__ + } + ) + ) + + def get_default_options(self) -> ConventionalCommitMonorepoParserOptions: + return ConventionalCommitMonorepoParserOptions() + + @staticmethod + def _process_path_filter_options( # noqa: C901 + path_filters: tuple[str, ...], + ) -> tuple[list[str], list[str]]: + file_ignore_filters: list[str] = [] + file_selection_filters: list[str] = [] + unique_selection_filters: set[str] = set() + unique_ignore_filters: set[str] = set() + + for str_path in path_filters: + str_filter = str_path[1:] if str_path.startswith("!") else str_path + filter_list = ( + file_ignore_filters + if str_path.startswith("!") + else file_selection_filters + ) + unique_cache = ( + unique_ignore_filters + if str_path.startswith("!") + else unique_selection_filters + ) + + # Since fnmatch is not too flexible, we will expand the path filters to include the name and any subdirectories + # as this is how gitignore is interpreted. Possible scenarios: + # | Input | Path Normalization | Filter List | + # | ---------- | ------------------ | ------------------------- | + # | / | / | /** | done + # | /./ | / | /** | done + # | /** | /** | /** | done + # | /./** | /** | /** | done + # | /* | /* | /* | done + # | . | . | ./** | done + # | ./ | . | ./** | done + # | ././ | . | ./** | done + # | ./** | ./** | ./** | done + # | ./* | ./* | ./* | done + # | .. | .. | ../** | done + # | ../ | .. | ../** | done + # | ../** | ../** | ../** | done + # | ../* | ../* | ../* | done + # | ../.. | ../.. | ../../** | done + # | ../../ | ../../ | ../../** | done + # | ../../docs | ../../docs | ../../docs, ../../docs/** | done + # | src | src | src, src/** | done + # | src/ | src | src/** | done + # | src/* | src/* | src/* | done + # | src/** | src/** | src/** | done + # | /src | /src | /src, /src/** | done + # | /src/ | /src | /src/** | done + # | /src/** | /src/** | /src/** | done + # | /src/* | /src/* | /src/* | done + # | ../d/f.txt | ../d/f.txt | ../d/f.txt, ../d/f.txt/** | done + # This expansion will occur regardless of the negation prefix + + os_path: PurePath | PurePosixPath | PureWindowsPath = PurePath(str_filter) + + if r"\\" in str_filter: + # Windows paths were given so we convert them to posix paths + os_path = PureWindowsPath(str_filter) + os_path = ( + PureWindowsPath( + os_path.root, *os_path.parts[1:] + ) # drop any drive letter + if os_path.is_absolute() + else os_path + ) + os_path = PurePosixPath(os_path.as_posix()) + + path_normalized = str(os_path) + if path_normalized == str( + Path(".").absolute().root + ) or path_normalized == str(Path("/**")): + path_normalized = "/**" + + elif path_normalized == str(Path("/*")): + pass + + elif path_normalized == str(Path(".")) or path_normalized == str( + Path("./**") + ): + path_normalized = "./**" + + elif path_normalized == str(Path("./*")): + path_normalized = "./*" + + elif path_normalized == str(Path("..")) or path_normalized == str( + Path("../**") + ): + path_normalized = "../**" + + elif path_normalized == str(Path("../*")): + path_normalized = "../*" + + elif path_normalized.endswith(("..", "../**")): + path_normalized = f"{path_normalized.rstrip('*')}/**" + + elif str_filter.endswith(os.sep): + # If the path ends with a separator, it is a directory, so we add the directory and all subdirectories + path_normalized = f"{path_normalized}/**" + + elif not path_normalized.endswith("*"): + all_subdirs = f"{path_normalized}/**" + if all_subdirs not in unique_cache: + unique_cache.add(all_subdirs) + filter_list.append(all_subdirs) + # And fall through to add the path as is + + # END IF + + # Add the normalized path to the filter list if it is not already present + if path_normalized not in unique_cache: + unique_cache.add(path_normalized) + filter_list.append(path_normalized) + + return file_selection_filters, file_ignore_filters + + def logged_parse_error(self, commit: Commit, error: str) -> ParseError: + self._logger.debug(error) + return ParseError(commit, error=error) + + def parse(self, commit: Commit) -> ParseResult | list[ParseResult]: + if self.options.ignore_merge_commits and self._base_parser.is_merge_commit( + commit + ): + return self._base_parser.log_parse_error( + commit, "Ignoring merge commit: %s" % commit.hexsha[:8] + ) + + separate_commits: list[Commit] = ( + self._base_parser.unsquash_commit(commit) + if self.options.parse_squash_commits + else [commit] + ) + + # Parse each commit individually if there were more than one + parsed_commits: list[ParseResult] = list( + map(self.parse_commit, separate_commits) + ) + + def add_linked_merge_request( + parsed_result: ParseResult, mr_number: str + ) -> ParseResult: + return ( + parsed_result + if not isinstance(parsed_result, ParsedCommit) + else ParsedCommit( + **{ + **parsed_result._asdict(), + "linked_merge_request": mr_number, + } + ) + ) + + # TODO: improve this for other VCS systems other than GitHub & BitBucket + # Github works as the first commit in a squash merge commit has the PR number + # appended to the first line of the commit message + lead_commit = next(iter(parsed_commits)) + + if isinstance(lead_commit, ParsedCommit) and lead_commit.linked_merge_request: + # If the first commit has linked merge requests, assume all commits + # are part of the same PR and add the linked merge requests to all + # parsed commits + parsed_commits = [ + lead_commit, + *map( + lambda parsed_result, mr=lead_commit.linked_merge_request: ( # type: ignore[misc] + add_linked_merge_request(parsed_result, mr) + ), + parsed_commits[1:], + ), + ] + + elif isinstance(lead_commit, ParseError) and ( + mr_match := self._base_parser.mr_selector.search( + force_str(lead_commit.message) + ) + ): + # Handle BitBucket Squash Merge Commits (see #1085), which have non angular commit + # format but include the PR number in the commit subject that we want to extract + linked_merge_request = mr_match.group("mr_number") + + # apply the linked MR to all commits + parsed_commits = [ + add_linked_merge_request(parsed_result, linked_merge_request) + for parsed_result in parsed_commits + ] + + return parsed_commits + + def parse_message( + self, message: str, strict_scope: bool = False + ) -> ParsedMessageResult | None: + if ( + not (parsed_match := self._strict_scope_pattern.match(message)) + and strict_scope + ): + return None + + if not parsed_match and not ( + parsed_match := self._optional_scope_pattern.match(message) + ): + return None + + return self._base_parser.create_parsed_message_result(parsed_match) + + def parse_commit(self, commit: Commit) -> ParseResult: + """Attempt to parse the commit message with a regular expression into a ParseResult.""" + # Multiple scenarios to consider when parsing a commit message [Truth table]: + # ======================================================================================================= + # | || INPUTS || | + # | # ||------------------------+----------------+--------------|| Result | + # | || Example Commit Message | Relevant Files | Scope Prefix || | + # |----||------------------------+----------------+--------------||-------------------------------------| + # | 1 || type(prefix-cli): msg | yes | "prefix-" || ParsedCommit | + # | 2 || type(prefix-cli): msg | yes | "" || ParsedCommit | + # | 3 || type(prefix-cli): msg | no | "prefix-" || ParsedCommit | + # | 4 || type(prefix-cli): msg | no | "" || ParseError[No files] | + # | 5 || type(scope-cli): msg | yes | "prefix-" || ParsedCommit | + # | 6 || type(scope-cli): msg | yes | "" || ParsedCommit | + # | 7 || type(scope-cli): msg | no | "prefix-" || ParseError[No files & wrong scope] | + # | 8 || type(scope-cli): msg | no | "" || ParseError[No files] | + # | 9 || type(cli): msg | yes | "prefix-" || ParsedCommit | + # | 10 || type(cli): msg | yes | "" || ParsedCommit | + # | 11 || type(cli): msg | no | "prefix-" || ParseError[No files & wrong scope] | + # | 12 || type(cli): msg | no | "" || ParseError[No files] | + # | 13 || type: msg | yes | "prefix-" || ParsedCommit | + # | 14 || type: msg | yes | "" || ParsedCommit | + # | 15 || type: msg | no | "prefix-" || ParseError[No files & wrong scope] | + # | 16 || type: msg | no | "" || ParseError[No files] | + # | 17 || non-conventional msg | yes | "prefix-" || ParseError[Invalid Syntax] | + # | 18 || non-conventional msg | yes | "" || ParseError[Invalid Syntax] | + # | 19 || non-conventional msg | no | "prefix-" || ParseError[Invalid Syntax] | + # | 20 || non-conventional msg | no | "" || ParseError[Invalid Syntax] | + # ======================================================================================================= + + # Initial Logic Flow: + # [1] When there are no relevant files and a scope prefix is defined, we enforce a strict scope + # [2] When there are no relevant files and no scope prefix is defined, we parse scoped or unscoped commits + # [3] When there are relevant files, we parse scoped or unscoped commits regardless of any defined prefix + has_relevant_changed_files = self._has_relevant_changed_files(commit) + strict_scope = bool( + not has_relevant_changed_files and self.options.scope_prefix + ) + pmsg_result = self.parse_message( + message=force_str(commit.message), + strict_scope=strict_scope, + ) + + if pmsg_result and (has_relevant_changed_files or strict_scope): + self._logger.debug( + "commit %s introduces a %s level_bump", + commit.hexsha[:8], + pmsg_result.bump, + ) + + return ParsedCommit.from_parsed_message_result(commit, pmsg_result) + + if pmsg_result and not has_relevant_changed_files: + return self.logged_parse_error( + commit, + f"Commit {commit.hexsha[:7]} has no changed files matching the path filter(s)", + ) + + if strict_scope and self.parse_message(str(commit.message), strict_scope=False): + return self.logged_parse_error( + commit, + str.join( + " and ", + [ + f"Commit {commit.hexsha[:7]} has no changed files matching the path filter(s)", + f"the scope does not match scope prefix '{self.options.scope_prefix}'", + ], + ), + ) + + return self.logged_parse_error( + commit, + f"Format Mismatch! Unable to parse commit message: {commit.message!r}", + ) + + def unsquash_commit_message(self, message: str) -> list[str]: + return self._base_parser.unsquash_commit_message(message) + + def _has_relevant_changed_files(self, commit: Commit) -> bool: + # Extract git root from commit + git_root = ( + Path(commit.repo.working_tree_dir or commit.repo.working_dir) + .absolute() + .resolve() + ) + + cwd = Path.cwd().absolute().resolve() + + rel_cwd = cwd.relative_to(git_root) if git_root in cwd.parents else Path(".") + + sandboxed_selection_filters: list[str] = [ + str(file_filter) + for file_filter in ( + ( + git_root / select_filter.rstrip("/") + if Path(select_filter).is_absolute() + else git_root / rel_cwd / select_filter + ) + for select_filter in self._file_selection_filters + ) + if git_root in file_filter.parents + ] + + sandboxed_ignore_filters: list[str] = [ + str(file_filter) + for file_filter in ( + ( + git_root / ignore_filter.rstrip("/") + if Path(ignore_filter).is_absolute() + else git_root / rel_cwd / ignore_filter + ) + for ignore_filter in self._file_ignore_filters + ) + if git_root in file_filter.parents + ] + + # Check if the changed files of the commit that match the path filters + for full_path in iter( + str(git_root / rel_git_path) for rel_git_path in commit.stats.files + ): + # Check if the filepath matches any of the file selection filters + if not any( + fnmatch(full_path, select_filter) + for select_filter in sandboxed_selection_filters + ): + continue + + # Pass filter matches, so now evaluate if it is supposed to be ignored + if not any( + fnmatch(full_path, ignore_filter) + for ignore_filter in sandboxed_ignore_filters + ): + # No ignore filter matched, so it must be a relevant file + return True + + return False diff --git a/tests/const.py b/tests/const.py index c7cc0a8b4..69a7ca778 100644 --- a/tests/const.py +++ b/tests/const.py @@ -11,6 +11,9 @@ class RepoActionStep(str, Enum): CONFIGURE = "CONFIGURE" + CONFIGURE_MONOREPO = "CONFIGURE_MONOREPO" + CREATE_MONOREPO = "CREATE_MONOREPO" + CHANGE_DIRECTORY = "CHANGE_DIRECTORY" WRITE_CHANGELOGS = "WRITE_CHANGELOGS" GIT_CHECKOUT = "GIT_CHECKOUT" GIT_COMMIT = "GIT_COMMIT" diff --git a/tests/e2e/cmd_changelog/test_changelog.py b/tests/e2e/cmd_changelog/test_changelog.py index 757e1316b..0ff1bc710 100644 --- a/tests/e2e/cmd_changelog/test_changelog.py +++ b/tests/e2e/cmd_changelog/test_changelog.py @@ -76,7 +76,7 @@ from requests_mock import Mocker - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser diff --git a/tests/e2e/cmd_changelog/test_changelog_custom_parser.py b/tests/e2e/cmd_changelog/test_changelog_custom_parser.py index 72b430875..5aa199bcb 100644 --- a/tests/e2e/cmd_changelog/test_changelog_custom_parser.py +++ b/tests/e2e/cmd_changelog/test_changelog_custom_parser.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from pathlib import Path - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) diff --git a/tests/e2e/cmd_version/bump_version/conftest.py b/tests/e2e/cmd_version/bump_version/conftest.py index 3af0678df..04a5209ce 100644 --- a/tests/e2e/cmd_version/bump_version/conftest.py +++ b/tests/e2e/cmd_version/bump_version/conftest.py @@ -18,13 +18,22 @@ from tests.conftest import RunCliFn from tests.fixtures.example_project import UpdatePyprojectTomlFn - from tests.fixtures.git_repo import BuildRepoFromDefinitionFn, RepoActionConfigure + from tests.fixtures.git_repo import ( + BuildRepoFromDefinitionFn, + RepoActionConfigure, + RepoActionConfigureMonorepo, + RepoActionCreateMonorepo, + ) class InitMirrorRepo4RebuildFn(Protocol): def __call__( self, mirror_repo_dir: Path, - configuration_steps: Sequence[RepoActionConfigure], + configuration_steps: Sequence[ + RepoActionConfigure + | RepoActionCreateMonorepo + | RepoActionConfigureMonorepo + ], files_to_remove: Sequence[Path], ) -> Path: ... @@ -43,7 +52,9 @@ def init_mirror_repo_for_rebuild( ) -> InitMirrorRepo4RebuildFn: def _init_mirror_repo_for_rebuild( mirror_repo_dir: Path, - configuration_steps: Sequence[RepoActionConfigure], + configuration_steps: Sequence[ + RepoActionConfigure | RepoActionCreateMonorepo | RepoActionConfigureMonorepo + ], files_to_remove: Sequence[Path], ) -> Path: # Create the mirror repo directory diff --git a/tests/e2e/cmd_version/bump_version/github_flow_monorepo/__init__.py b/tests/e2e/cmd_version/bump_version/github_flow_monorepo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/e2e/cmd_version/bump_version/github_flow_monorepo/test_monorepo_1_channel.py b/tests/e2e/cmd_version/bump_version/github_flow_monorepo/test_monorepo_1_channel.py new file mode 100644 index 000000000..fd5fb5ff2 --- /dev/null +++ b/tests/e2e/cmd_version/bump_version/github_flow_monorepo/test_monorepo_1_channel.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, cast + +import pytest +from freezegun import freeze_time + +from semantic_release.version.version import Version + +from tests.const import RepoActionStep +from tests.fixtures.monorepos.github_flow import ( + monorepo_w_github_flow_w_default_release_channel_conventional_commits, +) +from tests.util import temporary_working_directory + +if TYPE_CHECKING: + from typing import Literal, Sequence + from unittest.mock import MagicMock + + from requests_mock import Mocker + + from tests.e2e.cmd_version.bump_version.conftest import ( + InitMirrorRepo4RebuildFn, + RunPSReleaseFn, + ) + from tests.e2e.conftest import GetSanitizedChangelogContentFn + from tests.fixtures.example_project import ExProjectDir + from tests.fixtures.git_repo import ( + BuildRepoFromDefinitionFn, + BuildSpecificRepoFn, + CommitConvention, + GetGitRepo4DirFn, + RepoActionConfigure, + RepoActionConfigureMonorepo, + RepoActionCreateMonorepo, + RepoActionRelease, + RepoActions, + SplitRepoActionsByReleaseTagsFn, + ) + + +@pytest.mark.parametrize( + "repo_fixture_name", + [ + pytest.param(repo_fixture_name, marks=pytest.mark.comprehensive) + for repo_fixture_name in [ + monorepo_w_github_flow_w_default_release_channel_conventional_commits.__name__, + ] + ], +) +def test_githubflow_monorepo_rebuild_1_channel( + repo_fixture_name: str, + run_psr_release: RunPSReleaseFn, + build_monorepo_w_github_flow_w_default_release_channel: BuildSpecificRepoFn, + split_repo_actions_by_release_tags: SplitRepoActionsByReleaseTagsFn, + init_mirror_repo_for_rebuild: InitMirrorRepo4RebuildFn, + example_project_dir: ExProjectDir, + git_repo_for_directory: GetGitRepo4DirFn, + build_repo_from_definition: BuildRepoFromDefinitionFn, + mocked_git_push: MagicMock, + post_mocker: Mocker, + get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, + get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + monorepo_pkg1_pyproject_toml_file: Path, + monorepo_pkg2_pyproject_toml_file: Path, + monorepo_pkg1_version_py_file: Path, + monorepo_pkg2_version_py_file: Path, + monorepo_pkg1_changelog_md_file: Path, + monorepo_pkg2_changelog_md_file: Path, + monorepo_pkg1_changelog_rst_file: Path, + monorepo_pkg2_changelog_rst_file: Path, +): + # build target repo into a temporary directory + target_repo_dir = example_project_dir / repo_fixture_name + commit_type = cast( + "CommitConvention", repo_fixture_name.split("commits", 1)[0].split("_")[-2] + ) + target_repo_definition = build_monorepo_w_github_flow_w_default_release_channel( + repo_name=repo_fixture_name, + commit_type=commit_type, + dest_dir=target_repo_dir, + ) + target_git_repo = git_repo_for_directory(target_repo_dir) + + # split repo actions by release actions + release_tags_2_steps = split_repo_actions_by_release_tags(target_repo_definition) + + configuration_steps = cast( + "Sequence[RepoActionConfigure | RepoActionCreateMonorepo | RepoActionConfigureMonorepo]", + release_tags_2_steps.pop(None), + ) + + release_versions_2_steps = cast( + "dict[Version | Literal['Unreleased'], list[RepoActions]]", + release_tags_2_steps, + ) + + # Create the mirror repo directory + mirror_repo_dir = init_mirror_repo_for_rebuild( + mirror_repo_dir=(example_project_dir / "mirror"), + configuration_steps=configuration_steps, + files_to_remove=[], + ) + + mirror_git_repo = git_repo_for_directory(mirror_repo_dir) + + # rebuild repo from scratch stopping before each release tag + for curr_release_key, steps in release_versions_2_steps.items(): + curr_release_str = ( + curr_release_key.as_tag() + if isinstance(curr_release_key, Version) + else curr_release_key + ) + + # make sure mocks are clear + mocked_git_push.reset_mock() + post_mocker.reset_mock() + + # Extract expected result from target repo + if curr_release_str != "Unreleased": + target_git_repo.git.checkout(curr_release_str, detach=True, force=True) + + expected_pkg1_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=target_repo_dir, changelog_file=monorepo_pkg1_changelog_md_file + ) + expected_pkg2_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=target_repo_dir, changelog_file=monorepo_pkg2_changelog_md_file + ) + expected_pkg1_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=target_repo_dir, changelog_file=monorepo_pkg1_changelog_rst_file + ) + expected_pkg2_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=target_repo_dir, changelog_file=monorepo_pkg2_changelog_rst_file + ) + expected_pkg1_pyproject_toml_content = ( + target_repo_dir / monorepo_pkg1_pyproject_toml_file + ).read_text() + expected_pkg2_pyproject_toml_content = ( + target_repo_dir / monorepo_pkg2_pyproject_toml_file + ).read_text() + expected_pkg1_version_file_content = ( + target_repo_dir / monorepo_pkg1_version_py_file + ).read_text() + expected_pkg2_version_file_content = ( + target_repo_dir / monorepo_pkg2_version_py_file + ).read_text() + expected_release_commit_text = target_git_repo.head.commit.message + + # In our repo env, start building the repo from the definition + build_repo_from_definition( + dest_dir=mirror_repo_dir, + # stop before the release step + repo_construction_steps=steps[ + : -1 if curr_release_str != "Unreleased" else None + ], + ) + + release_directory = mirror_repo_dir + + for step in steps[::-1]: # reverse order + if step["action"] == RepoActionStep.CHANGE_DIRECTORY: + release_directory = ( + mirror_repo_dir + if str(Path(step["details"]["directory"])) + == str(mirror_repo_dir.root) + else Path(step["details"]["directory"]) + ) + + release_directory = ( + mirror_repo_dir / release_directory + if not release_directory.is_absolute() + else release_directory + ) + + if mirror_repo_dir not in release_directory.parents: + release_directory = mirror_repo_dir + + break + + # Act: run PSR on the repo instead of the RELEASE step + if curr_release_str != "Unreleased": + release_action_step = cast("RepoActionRelease", steps[-1]) + + with freeze_time( + release_action_step["details"]["datetime"] + ), temporary_working_directory(release_directory): + run_psr_release( + next_version_str=release_action_step["details"]["version"], + git_repo=mirror_git_repo, + config_toml_path=Path("pyproject.toml"), + ) + else: + # run psr changelog command to validate changelog + pass + + # take measurement after running the version command + actual_release_commit_text = mirror_git_repo.head.commit.message + actual_pkg1_pyproject_toml_content = ( + mirror_repo_dir / monorepo_pkg1_pyproject_toml_file + ).read_text() + actual_pkg2_pyproject_toml_content = ( + mirror_repo_dir / monorepo_pkg2_pyproject_toml_file + ).read_text() + actual_pkg1_version_file_content = ( + mirror_repo_dir / monorepo_pkg1_version_py_file + ).read_text() + actual_pkg2_version_file_content = ( + mirror_repo_dir / monorepo_pkg2_version_py_file + ).read_text() + actual_pkg1_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=mirror_repo_dir, changelog_file=monorepo_pkg1_changelog_md_file + ) + actual_pkg2_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=mirror_repo_dir, changelog_file=monorepo_pkg2_changelog_md_file + ) + actual_pkg1_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=mirror_repo_dir, changelog_file=monorepo_pkg1_changelog_rst_file + ) + actual_pkg2_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=mirror_repo_dir, changelog_file=monorepo_pkg2_changelog_rst_file + ) + + # Evaluate (normal release actions should have occurred as expected) + # ------------------------------------------------------------------ + # Make sure version file is updated + assert ( + expected_pkg1_pyproject_toml_content == actual_pkg1_pyproject_toml_content + ) + assert ( + expected_pkg2_pyproject_toml_content == actual_pkg2_pyproject_toml_content + ) + assert expected_pkg1_version_file_content == actual_pkg1_version_file_content + assert expected_pkg2_version_file_content == actual_pkg2_version_file_content + + # Make sure changelog is updated + assert expected_pkg1_md_changelog_content == actual_pkg1_md_changelog_content + assert expected_pkg2_md_changelog_content == actual_pkg2_md_changelog_content + assert expected_pkg1_rst_changelog_content == actual_pkg1_rst_changelog_content + assert expected_pkg2_rst_changelog_content == actual_pkg2_rst_changelog_content + + # Make sure commit is created + assert expected_release_commit_text == actual_release_commit_text + + if curr_release_str != "Unreleased": + # Make sure tag is created + assert curr_release_str in [tag.name for tag in mirror_git_repo.tags] + + # Make sure publishing actions occurred + assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/bump_version/github_flow_monorepo/test_monorepo_2_channels.py b/tests/e2e/cmd_version/bump_version/github_flow_monorepo/test_monorepo_2_channels.py new file mode 100644 index 000000000..57d6cd3fb --- /dev/null +++ b/tests/e2e/cmd_version/bump_version/github_flow_monorepo/test_monorepo_2_channels.py @@ -0,0 +1,250 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, cast + +import pytest +from freezegun import freeze_time + +from semantic_release.version.version import Version + +from tests.const import RepoActionStep +from tests.fixtures.monorepos.github_flow import ( + monorepo_w_github_flow_w_feature_release_channel_conventional_commits, +) +from tests.util import temporary_working_directory + +if TYPE_CHECKING: + from typing import Literal, Sequence + from unittest.mock import MagicMock + + from requests_mock import Mocker + + from tests.e2e.cmd_version.bump_version.conftest import ( + InitMirrorRepo4RebuildFn, + RunPSReleaseFn, + ) + from tests.e2e.conftest import GetSanitizedChangelogContentFn + from tests.fixtures.example_project import ExProjectDir + from tests.fixtures.git_repo import ( + BuildRepoFromDefinitionFn, + BuildSpecificRepoFn, + CommitConvention, + GetGitRepo4DirFn, + RepoActionConfigure, + RepoActionConfigureMonorepo, + RepoActionCreateMonorepo, + RepoActionRelease, + RepoActions, + SplitRepoActionsByReleaseTagsFn, + ) + + +@pytest.mark.parametrize( + "repo_fixture_name", + [ + pytest.param(repo_fixture_name, marks=pytest.mark.comprehensive) + for repo_fixture_name in [ + monorepo_w_github_flow_w_feature_release_channel_conventional_commits.__name__, + ] + ], +) +def test_githubflow_monorepo_rebuild_2_channels( + repo_fixture_name: str, + run_psr_release: RunPSReleaseFn, + build_monorepo_w_github_flow_w_feature_release_channel: BuildSpecificRepoFn, + split_repo_actions_by_release_tags: SplitRepoActionsByReleaseTagsFn, + init_mirror_repo_for_rebuild: InitMirrorRepo4RebuildFn, + example_project_dir: ExProjectDir, + git_repo_for_directory: GetGitRepo4DirFn, + build_repo_from_definition: BuildRepoFromDefinitionFn, + mocked_git_push: MagicMock, + post_mocker: Mocker, + get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, + get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + monorepo_pkg1_pyproject_toml_file: Path, + monorepo_pkg2_pyproject_toml_file: Path, + monorepo_pkg1_version_py_file: Path, + monorepo_pkg2_version_py_file: Path, + monorepo_pkg1_changelog_md_file: Path, + monorepo_pkg2_changelog_md_file: Path, + monorepo_pkg1_changelog_rst_file: Path, + monorepo_pkg2_changelog_rst_file: Path, +): + # build target repo into a temporary directory + target_repo_dir = example_project_dir / repo_fixture_name + commit_type = cast( + "CommitConvention", repo_fixture_name.split("commits", 1)[0].split("_")[-2] + ) + target_repo_definition = build_monorepo_w_github_flow_w_feature_release_channel( + repo_name=repo_fixture_name, + commit_type=commit_type, + dest_dir=target_repo_dir, + ) + target_git_repo = git_repo_for_directory(target_repo_dir) + + # split repo actions by release actions + release_tags_2_steps = split_repo_actions_by_release_tags(target_repo_definition) + + configuration_steps = cast( + "Sequence[RepoActionConfigure | RepoActionCreateMonorepo | RepoActionConfigureMonorepo]", + release_tags_2_steps.pop(None), + ) + + release_versions_2_steps = cast( + "dict[Version | Literal['Unreleased'], list[RepoActions]]", + release_tags_2_steps, + ) + + # Create the mirror repo directory + mirror_repo_dir = init_mirror_repo_for_rebuild( + mirror_repo_dir=(example_project_dir / "mirror"), + configuration_steps=configuration_steps, + files_to_remove=[], + ) + mirror_git_repo = git_repo_for_directory(mirror_repo_dir) + + # rebuild repo from scratch stopping before each release tag + for curr_release_key, steps in release_versions_2_steps.items(): + curr_release_str = ( + curr_release_key.as_tag() + if isinstance(curr_release_key, Version) + else curr_release_key + ) + + # make sure mocks are clear + mocked_git_push.reset_mock() + post_mocker.reset_mock() + + # Extract expected result from target repo + if curr_release_str != "Unreleased": + target_git_repo.git.checkout(curr_release_str, detach=True, force=True) + + expected_pkg1_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=target_repo_dir, changelog_file=monorepo_pkg1_changelog_md_file + ) + expected_pkg2_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=target_repo_dir, changelog_file=monorepo_pkg2_changelog_md_file + ) + expected_pkg1_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=target_repo_dir, changelog_file=monorepo_pkg1_changelog_rst_file + ) + expected_pkg2_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=target_repo_dir, changelog_file=monorepo_pkg2_changelog_rst_file + ) + expected_pkg1_pyproject_toml_content = ( + target_repo_dir / monorepo_pkg1_pyproject_toml_file + ).read_text() + expected_pkg2_pyproject_toml_content = ( + target_repo_dir / monorepo_pkg2_pyproject_toml_file + ).read_text() + expected_pkg1_version_file_content = ( + target_repo_dir / monorepo_pkg1_version_py_file + ).read_text() + expected_pkg2_version_file_content = ( + target_repo_dir / monorepo_pkg2_version_py_file + ).read_text() + expected_release_commit_text = target_git_repo.head.commit.message + + # In our repo env, start building the repo from the definition + build_repo_from_definition( + dest_dir=mirror_repo_dir, + # stop before the release step + repo_construction_steps=steps[ + : -1 if curr_release_str != "Unreleased" else None + ], + ) + + release_directory = mirror_repo_dir + + for step in steps[::-1]: # reverse order + if step["action"] == RepoActionStep.CHANGE_DIRECTORY: + release_directory = ( + mirror_repo_dir + if str(Path(step["details"]["directory"])) + == str(mirror_repo_dir.root) + else Path(step["details"]["directory"]) + ) + + release_directory = ( + mirror_repo_dir / release_directory + if not release_directory.is_absolute() + else release_directory + ) + + if mirror_repo_dir not in release_directory.parents: + release_directory = mirror_repo_dir + + break + + # Act: run PSR on the repo instead of the RELEASE step + if curr_release_str != "Unreleased": + release_action_step = cast("RepoActionRelease", steps[-1]) + + with freeze_time( + release_action_step["details"]["datetime"] + ), temporary_working_directory(release_directory): + run_psr_release( + next_version_str=release_action_step["details"]["version"], + git_repo=mirror_git_repo, + config_toml_path=Path("pyproject.toml"), + ) + else: + # run psr changelog command to validate changelog + pass + + # take measurement after running the version command + actual_release_commit_text = mirror_git_repo.head.commit.message + actual_pkg1_pyproject_toml_content = ( + mirror_repo_dir / monorepo_pkg1_pyproject_toml_file + ).read_text() + actual_pkg2_pyproject_toml_content = ( + mirror_repo_dir / monorepo_pkg2_pyproject_toml_file + ).read_text() + actual_pkg1_version_file_content = ( + mirror_repo_dir / monorepo_pkg1_version_py_file + ).read_text() + actual_pkg2_version_file_content = ( + mirror_repo_dir / monorepo_pkg2_version_py_file + ).read_text() + actual_pkg1_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=mirror_repo_dir, changelog_file=monorepo_pkg1_changelog_md_file + ) + actual_pkg2_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=mirror_repo_dir, changelog_file=monorepo_pkg2_changelog_md_file + ) + actual_pkg1_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=mirror_repo_dir, changelog_file=monorepo_pkg1_changelog_rst_file + ) + actual_pkg2_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=mirror_repo_dir, changelog_file=monorepo_pkg2_changelog_rst_file + ) + + # Evaluate (normal release actions should have occurred as expected) + # ------------------------------------------------------------------ + # Make sure version file is updated + assert ( + expected_pkg1_pyproject_toml_content == actual_pkg1_pyproject_toml_content + ) + assert ( + expected_pkg2_pyproject_toml_content == actual_pkg2_pyproject_toml_content + ) + assert expected_pkg1_version_file_content == actual_pkg1_version_file_content + assert expected_pkg2_version_file_content == actual_pkg2_version_file_content + + # Make sure changelog is updated + assert expected_pkg1_md_changelog_content == actual_pkg1_md_changelog_content + assert expected_pkg2_md_changelog_content == actual_pkg2_md_changelog_content + assert expected_pkg1_rst_changelog_content == actual_pkg1_rst_changelog_content + assert expected_pkg2_rst_changelog_content == actual_pkg2_rst_changelog_content + + # Make sure commit is created + assert expected_release_commit_text == actual_release_commit_text + + if curr_release_str != "Unreleased": + # Make sure tag is created + assert curr_release_str in [tag.name for tag in mirror_git_repo.tags] + + # Make sure publishing actions occurred + assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/e2e/cmd_version/bump_version/trunk_based_dev_monorepo/__init__.py b/tests/e2e/cmd_version/bump_version/trunk_based_dev_monorepo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/e2e/cmd_version/bump_version/trunk_based_dev_monorepo/test_monorepo_trunk.py b/tests/e2e/cmd_version/bump_version/trunk_based_dev_monorepo/test_monorepo_trunk.py new file mode 100644 index 000000000..ec4ccd60a --- /dev/null +++ b/tests/e2e/cmd_version/bump_version/trunk_based_dev_monorepo/test_monorepo_trunk.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, cast + +import pytest +from freezegun import freeze_time + +from semantic_release.version.version import Version + +from tests.const import RepoActionStep +from tests.fixtures.monorepos.trunk_based_dev import ( + monorepo_w_trunk_only_releases_conventional_commits, +) +from tests.util import temporary_working_directory + +if TYPE_CHECKING: + from typing import Literal, Sequence + from unittest.mock import MagicMock + + from requests_mock import Mocker + + from tests.e2e.cmd_version.bump_version.conftest import ( + InitMirrorRepo4RebuildFn, + RunPSReleaseFn, + ) + from tests.e2e.conftest import GetSanitizedChangelogContentFn + from tests.fixtures.example_project import ExProjectDir + from tests.fixtures.git_repo import ( + BuildRepoFromDefinitionFn, + BuildSpecificRepoFn, + CommitConvention, + GetGitRepo4DirFn, + RepoActionConfigure, + RepoActionConfigureMonorepo, + RepoActionCreateMonorepo, + RepoActionRelease, + RepoActions, + SplitRepoActionsByReleaseTagsFn, + ) + + +@pytest.mark.parametrize( + "repo_fixture_name", + [ + pytest.param(repo_fixture_name, marks=pytest.mark.comprehensive) + for repo_fixture_name in [ + monorepo_w_trunk_only_releases_conventional_commits.__name__, + ] + ], +) +def test_trunk_monorepo_rebuild_1_channel( + repo_fixture_name: str, + run_psr_release: RunPSReleaseFn, + build_trunk_only_monorepo_w_tags: BuildSpecificRepoFn, + split_repo_actions_by_release_tags: SplitRepoActionsByReleaseTagsFn, + init_mirror_repo_for_rebuild: InitMirrorRepo4RebuildFn, + example_project_dir: ExProjectDir, + git_repo_for_directory: GetGitRepo4DirFn, + build_repo_from_definition: BuildRepoFromDefinitionFn, + mocked_git_push: MagicMock, + post_mocker: Mocker, + get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, + get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, + monorepo_pkg1_pyproject_toml_file: Path, + monorepo_pkg2_pyproject_toml_file: Path, + monorepo_pkg1_version_py_file: Path, + monorepo_pkg2_version_py_file: Path, + monorepo_pkg1_changelog_md_file: Path, + monorepo_pkg2_changelog_md_file: Path, + monorepo_pkg1_changelog_rst_file: Path, + monorepo_pkg2_changelog_rst_file: Path, +): + # build target repo into a temporary directory + target_repo_dir = example_project_dir / repo_fixture_name + commit_type = cast( + "CommitConvention", repo_fixture_name.split("commits", 1)[0].split("_")[-2] + ) + target_repo_definition = build_trunk_only_monorepo_w_tags( + repo_name=repo_fixture_name, + commit_type=commit_type, + dest_dir=target_repo_dir, + ) + target_git_repo = git_repo_for_directory(target_repo_dir) + + # split repo actions by release actions + release_tags_2_steps = split_repo_actions_by_release_tags(target_repo_definition) + + configuration_steps = cast( + "Sequence[RepoActionConfigure | RepoActionCreateMonorepo | RepoActionConfigureMonorepo]", + release_tags_2_steps.pop(None), + ) + + release_versions_2_steps = cast( + "dict[Version | Literal['Unreleased'], list[RepoActions]]", + release_tags_2_steps, + ) + + # Create the mirror repo directory + mirror_repo_dir = init_mirror_repo_for_rebuild( + mirror_repo_dir=(example_project_dir / "mirror"), + configuration_steps=configuration_steps, + files_to_remove=[], + ) + + mirror_git_repo = git_repo_for_directory(mirror_repo_dir) + + # rebuild repo from scratch stopping before each release tag + for curr_release_key, steps in release_versions_2_steps.items(): + curr_release_str = ( + curr_release_key.as_tag() + if isinstance(curr_release_key, Version) + else curr_release_key + ) + + # make sure mocks are clear + mocked_git_push.reset_mock() + post_mocker.reset_mock() + + # Extract expected result from target repo + if curr_release_str != "Unreleased": + target_git_repo.git.checkout(curr_release_str, detach=True, force=True) + + expected_pkg1_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=target_repo_dir, changelog_file=monorepo_pkg1_changelog_md_file + ) + expected_pkg2_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=target_repo_dir, changelog_file=monorepo_pkg2_changelog_md_file + ) + expected_pkg1_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=target_repo_dir, changelog_file=monorepo_pkg1_changelog_rst_file + ) + expected_pkg2_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=target_repo_dir, changelog_file=monorepo_pkg2_changelog_rst_file + ) + expected_pkg1_pyproject_toml_content = ( + target_repo_dir / monorepo_pkg1_pyproject_toml_file + ).read_text() + expected_pkg2_pyproject_toml_content = ( + target_repo_dir / monorepo_pkg2_pyproject_toml_file + ).read_text() + expected_pkg1_version_file_content = ( + target_repo_dir / monorepo_pkg1_version_py_file + ).read_text() + expected_pkg2_version_file_content = ( + target_repo_dir / monorepo_pkg2_version_py_file + ).read_text() + expected_release_commit_text = target_git_repo.head.commit.message + + # In our repo env, start building the repo from the definition + build_repo_from_definition( + dest_dir=mirror_repo_dir, + # stop before the release step + repo_construction_steps=steps[ + : -1 if curr_release_str != "Unreleased" else None + ], + ) + + release_directory = mirror_repo_dir + + for step in steps[::-1]: # reverse order + if step["action"] == RepoActionStep.CHANGE_DIRECTORY: + release_directory = ( + mirror_repo_dir + if str(Path(step["details"]["directory"])) + == str(mirror_repo_dir.root) + else Path(step["details"]["directory"]) + ) + + release_directory = ( + mirror_repo_dir / release_directory + if not release_directory.is_absolute() + else release_directory + ) + + if mirror_repo_dir not in release_directory.parents: + release_directory = mirror_repo_dir + + break + + # Act: run PSR on the repo instead of the RELEASE step + if curr_release_str != "Unreleased": + release_action_step = cast("RepoActionRelease", steps[-1]) + + with freeze_time( + release_action_step["details"]["datetime"] + ), temporary_working_directory(release_directory): + run_psr_release( + next_version_str=release_action_step["details"]["version"], + git_repo=mirror_git_repo, + config_toml_path=Path("pyproject.toml"), + ) + else: + # run psr changelog command to validate changelog + pass + + # take measurement after running the version command + actual_release_commit_text = mirror_git_repo.head.commit.message + actual_pkg1_pyproject_toml_content = ( + mirror_repo_dir / monorepo_pkg1_pyproject_toml_file + ).read_text() + actual_pkg2_pyproject_toml_content = ( + mirror_repo_dir / monorepo_pkg2_pyproject_toml_file + ).read_text() + actual_pkg1_version_file_content = ( + mirror_repo_dir / monorepo_pkg1_version_py_file + ).read_text() + actual_pkg2_version_file_content = ( + mirror_repo_dir / monorepo_pkg2_version_py_file + ).read_text() + actual_pkg1_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=mirror_repo_dir, changelog_file=monorepo_pkg1_changelog_md_file + ) + actual_pkg2_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=mirror_repo_dir, changelog_file=monorepo_pkg2_changelog_md_file + ) + actual_pkg1_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=mirror_repo_dir, changelog_file=monorepo_pkg1_changelog_rst_file + ) + actual_pkg2_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=mirror_repo_dir, changelog_file=monorepo_pkg2_changelog_rst_file + ) + + # Evaluate (normal release actions should have occurred as expected) + # ------------------------------------------------------------------ + # Make sure version file is updated + assert ( + expected_pkg1_pyproject_toml_content == actual_pkg1_pyproject_toml_content + ) + assert ( + expected_pkg2_pyproject_toml_content == actual_pkg2_pyproject_toml_content + ) + assert expected_pkg1_version_file_content == actual_pkg1_version_file_content + assert expected_pkg2_version_file_content == actual_pkg2_version_file_content + + # Make sure changelog is updated + assert expected_pkg1_md_changelog_content == actual_pkg1_md_changelog_content + assert expected_pkg2_md_changelog_content == actual_pkg2_md_changelog_content + assert expected_pkg1_rst_changelog_content == actual_pkg1_rst_changelog_content + assert expected_pkg2_rst_changelog_content == actual_pkg2_rst_changelog_content + + # Make sure commit is created + assert expected_release_commit_text == actual_release_commit_text + + if curr_release_str != "Unreleased": + # Make sure tag is created + assert curr_release_str in [tag.name for tag in mirror_git_repo.tags] + + # Make sure publishing actions occurred + assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag + assert post_mocker.call_count == 1 # vcs release creation occurred diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index d9e987f57..fcf471c9b 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -1,5 +1,6 @@ from tests.fixtures.commit_parsers import * from tests.fixtures.example_project import * from tests.fixtures.git_repo import * +from tests.fixtures.monorepos import * from tests.fixtures.repos import * from tests.fixtures.scipy import * diff --git a/tests/fixtures/example_project.py b/tests/fixtures/example_project.py index 6b804fdae..41664f75a 100644 --- a/tests/fixtures/example_project.py +++ b/tests/fixtures/example_project.py @@ -24,6 +24,9 @@ EmojiCommitParser, ScipyCommitParser, ) +from semantic_release.commit_parser.conventional.parser_monorepo import ( + ConventionalCommitMonorepoParser, +) from semantic_release.hvcs import Bitbucket, Gitea, Github, Gitlab import tests.conftest @@ -83,7 +86,7 @@ def __call__( class UseParserFn(Protocol): def __call__( - self, toml_file: Path | str = ... + self, toml_file: Path | str = ..., monorepo: bool = ... ) -> type[CommitParser[ParseResult, ParserOptions]]: ... class UseReleaseNotesTemplateFn(Protocol): @@ -606,13 +609,16 @@ def use_conventional_parser( """Modify the configuration file to use the Conventional parser.""" def _use_conventional_parser( - toml_file: Path | str = pyproject_toml_file, + toml_file: Path | str = pyproject_toml_file, monorepo: bool = False ) -> type[CommitParser[ParseResult, ParserOptions]]: update_pyproject_toml( - pyproject_toml_config_option_parser, "conventional", toml_file=toml_file + pyproject_toml_config_option_parser, + f"conventional{'-monorepo' if monorepo else ''}", + toml_file=toml_file, ) return cast( - "type[CommitParser[ParseResult, ParserOptions]]", ConventionalCommitParser + "type[CommitParser[ParseResult, ParserOptions]]", + ConventionalCommitMonorepoParser if monorepo else ConventionalCommitParser, ) return _use_conventional_parser @@ -627,8 +633,14 @@ def use_emoji_parser( """Modify the configuration file to use the Emoji parser.""" def _use_emoji_parser( - toml_file: Path | str = pyproject_toml_file, + toml_file: Path | str = pyproject_toml_file, monorepo: bool = False ) -> type[CommitParser[ParseResult, ParserOptions]]: + if monorepo: + raise ValueError( + "The Emoji parser does not support monorepo mode. " + "Use the conventional parser instead." + ) + update_pyproject_toml( pyproject_toml_config_option_parser, "emoji", toml_file=toml_file ) @@ -646,8 +658,14 @@ def use_scipy_parser( """Modify the configuration file to use the Scipy parser.""" def _use_scipy_parser( - toml_file: Path | str = pyproject_toml_file, + toml_file: Path | str = pyproject_toml_file, monorepo: bool = False ) -> type[CommitParser[ParseResult, ParserOptions]]: + if monorepo: + raise ValueError( + "The Scipy parser does not support monorepo mode. " + "Use the conventional parser instead." + ) + update_pyproject_toml( pyproject_toml_config_option_parser, "scipy", toml_file=toml_file ) diff --git a/tests/fixtures/git_repo.py b/tests/fixtures/git_repo.py index f76f83455..e4051183f 100644 --- a/tests/fixtures/git_repo.py +++ b/tests/fixtures/git_repo.py @@ -64,6 +64,9 @@ from semantic_release.commit_parser.conventional import ( ConventionalCommitParser, ) + from semantic_release.commit_parser.conventional.parser_monorepo import ( + ConventionalCommitMonorepoParser, + ) 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 @@ -73,6 +76,7 @@ GetParserFromConfigFileFn, UpdateVersionPyFileFn, ) + from tests.fixtures.monorepos.git_monorepo import BuildMonorepoFn try: # Python 3.8 and 3.9 compatibility @@ -155,6 +159,7 @@ def __call__( extra_configs: dict[str, TomlSerializableTypes] | None = None, mask_initial_release: bool = True, # Default as of v10 package_name: str = ..., + monorepo: bool = False, ) -> tuple[Path, HvcsBase]: ... class CommitNReturnChangelogEntryFn(Protocol): @@ -301,6 +306,34 @@ class RepoActionConfigureDetails(DetailsBase): mask_initial_release: bool extra_configs: dict[str, TomlSerializableTypes] + class RepoActionConfigureMonorepo(TypedDict): + action: Literal[RepoActionStep.CONFIGURE_MONOREPO] + details: RepoActionConfigureMonorepoDetails + + class RepoActionConfigureMonorepoDetails(DetailsBase): + package_dir: Path | str + package_name: str + tag_format_str: str | None + mask_initial_release: bool + extra_configs: dict[str, TomlSerializableTypes] + + class RepoActionCreateMonorepo(TypedDict): + action: Literal[RepoActionStep.CREATE_MONOREPO] + details: RepoActionCreateMonorepoDetails + + class RepoActionCreateMonorepoDetails(DetailsBase): + commit_type: CommitConvention + hvcs_client_name: str + hvcs_domain: str + origin_url: NotRequired[str] + + class RepoActionChangeDirectory(TypedDict): + action: Literal[RepoActionStep.CHANGE_DIRECTORY] + details: RepoActionChangeDirectoryDetails + + class RepoActionChangeDirectoryDetails(DetailsBase): + directory: Path | str + class RepoActionMakeCommits(TypedDict): action: Literal[RepoActionStep.MAKE_COMMITS] details: RepoActionMakeCommitsDetails @@ -381,6 +414,7 @@ def __call__( commit_spec: CommitSpec, commit_type: CommitConvention, parser: CommitParser[ParseResult, ParserOptions], + monorepo: bool = ..., ) -> CommitDef: ... class GetRepoDefinitionFn(Protocol): @@ -415,6 +449,7 @@ def __call__( commits: Sequence[CommitSpec], commit_type: CommitConvention, parser: CommitParser[ParseResult, ParserOptions], + monorepo: bool = ..., ) -> Sequence[CommitDef]: ... class BuildSpecificRepoFn(Protocol): @@ -423,7 +458,10 @@ def __call__( ) -> Sequence[RepoActions]: ... RepoActions: TypeAlias = Union[ + RepoActionChangeDirectory, RepoActionConfigure, + RepoActionConfigureMonorepo, + RepoActionCreateMonorepo, RepoActionGitCheckout, RepoActionGitMerge[RepoActionGitMergeDetails], RepoActionGitMerge[RepoActionGitFFMergeDetails], @@ -611,6 +649,43 @@ def _get_commit_def(msg: str, parser: ConventionalCommitParser) -> CommitDef: return _get_commit_def +@pytest.fixture(scope="session") +def get_commit_def_of_conventional_commit_monorepo() -> ( + GetCommitDefFn[ConventionalCommitMonorepoParser] +): + def _get_commit_def( + msg: str, parser: ConventionalCommitMonorepoParser + ) -> CommitDef: + if not (parsed_result := parser.parse_message(msg)): + return { + "cid": "", + "msg": msg, + "type": "unknown", + "category": "Unknown", + "desc": msg, + "brking_desc": "", + "scope": "", + "mr": "", + "sha": NULL_HEX_SHA, + "include_in_changelog": False, + } + + return { + "cid": "", + "msg": msg, + "type": parsed_result.type, + "category": parsed_result.category, + "desc": str.join("\n\n", parsed_result.descriptions), + "brking_desc": str.join("\n\n", parsed_result.breaking_descriptions), + "scope": parsed_result.scope, + "mr": parsed_result.linked_merge_request, + "sha": NULL_HEX_SHA, + "include_in_changelog": True, + } + + return _get_commit_def + + @pytest.fixture(scope="session") def get_commit_def_of_emoji_commit() -> GetCommitDefFn[EmojiCommitParser]: def _get_commit_def_of_emoji_commit( @@ -1078,6 +1153,7 @@ def _build_configured_base_repo( # noqa: C901 extra_configs: dict[str, TomlSerializableTypes] | None = None, mask_initial_release: bool = True, # Default as of v10 package_name: str = EXAMPLE_PROJECT_NAME, + monorepo: bool = False, ) -> tuple[Path, HvcsBase]: if not cached_example_git_project.exists(): raise RuntimeError("Unable to find cached git project files!") @@ -1094,6 +1170,7 @@ def _build_configured_base_repo( # noqa: C901 extra_configs=extra_configs, mask_initial_release=mask_initial_release, package_name=package_name, + monorepo=monorepo, ) return _build_configured_base_repo @@ -1131,16 +1208,19 @@ def _configure_base_repo( # noqa: C901 extra_configs: dict[str, TomlSerializableTypes] | None = None, mask_initial_release: bool = True, # Default as of v10 package_name: str = EXAMPLE_PROJECT_NAME, + monorepo: bool = False, ) -> tuple[Path, HvcsBase]: # Make sure we are in the dest directory with temporary_working_directory(dest_dir): # Set parser configuration if commit_type == "conventional": - use_conventional_parser(toml_file=pyproject_toml_file) + use_conventional_parser( + toml_file=pyproject_toml_file, monorepo=monorepo + ) elif commit_type == "emoji": - use_emoji_parser(toml_file=pyproject_toml_file) + use_emoji_parser(toml_file=pyproject_toml_file, monorepo=monorepo) elif commit_type == "scipy": - use_scipy_parser(toml_file=pyproject_toml_file) + use_scipy_parser(toml_file=pyproject_toml_file, monorepo=monorepo) else: use_custom_parser(commit_type, toml_file=pyproject_toml_file) @@ -1290,12 +1370,16 @@ def _separate_squashed_commit_def( @pytest.fixture(scope="session") def convert_commit_spec_to_commit_def( get_commit_def_of_conventional_commit: GetCommitDefFn[ConventionalCommitParser], + get_commit_def_of_conventional_commit_monorepo: GetCommitDefFn[ + ConventionalCommitMonorepoParser + ], get_commit_def_of_emoji_commit: GetCommitDefFn[EmojiCommitParser], get_commit_def_of_scipy_commit: GetCommitDefFn[ScipyCommitParser], stable_now_date: datetime, ) -> ConvertCommitSpecToCommitDefFn: message_parsers = { "conventional": get_commit_def_of_conventional_commit, + "conventional-monorepo": get_commit_def_of_conventional_commit_monorepo, "emoji": get_commit_def_of_emoji_commit, "scipy": get_commit_def_of_scipy_commit, } @@ -1304,8 +1388,12 @@ def _convert( commit_spec: CommitSpec, commit_type: CommitConvention, parser: CommitParser[ParseResult, ParserOptions], + monorepo: bool = False, ) -> CommitDef: - parse_msg_fn = cast("GetCommitDefFn[Any]", message_parsers[commit_type]) + parse_msg_fn = cast( + "GetCommitDefFn[Any]", + message_parsers[f"{commit_type}{'-monorepo' if monorepo else ''}"], + ) # Extract the correct commit message for the commit type return { @@ -1330,9 +1418,12 @@ def _convert( commits: Sequence[CommitSpec], commit_type: CommitConvention, parser: CommitParser[ParseResult, ParserOptions], + monorepo: bool = False, ) -> Sequence[CommitDef]: return [ - convert_commit_spec_to_commit_def(commit, commit_type, parser=parser) + convert_commit_spec_to_commit_def( + commit, commit_type, parser=parser, monorepo=monorepo + ) for commit in commits ] @@ -1342,6 +1433,8 @@ def _convert( @pytest.fixture(scope="session") def build_repo_from_definition( # noqa: C901, its required and its just test code build_configured_base_repo: BuildRepoFn, + build_base_monorepo: BuildMonorepoFn, + configure_monorepo_package: BuildRepoFn, default_tag_format_str: str, create_release_tagged_commit: CreateReleaseFn, create_squash_merge_commit: CreateSquashMergeCommitFn, @@ -1386,6 +1479,7 @@ def _build_repo_from_definition( # noqa: C901, its required and its just test c ) repo_dir = Path(dest_dir).resolve().absolute() + commit_type: CommitConvention = "conventional" hvcs: Github | Gitlab | Gitea | Bitbucket commit_cache: dict[str, CommitDef] = {} current_repo_def: dict[Version | Literal["Unreleased"], RepoVersionDef] = {} @@ -1418,6 +1512,64 @@ def _build_repo_from_definition( # noqa: C901, its required and its just test c }, ) + elif action == RepoActionStep.CREATE_MONOREPO: + cfg_mr_def = cast( + "RepoActionCreateMonorepoDetails", step_result["details"] + ) + build_base_monorepo(dest_dir=repo_dir) + hvcs = get_hvcs( + hvcs_client_name=cfg_mr_def["hvcs_client_name"], + origin_url=cfg_mr_def.get("origin_url") + or example_git_https_url, + hvcs_domain=cfg_mr_def["hvcs_domain"], + ) + commit_type = cfg_mr_def["commit_type"] + + elif action == RepoActionStep.CONFIGURE_MONOREPO: + cfg_mr_pkg_def = cast( + "RepoActionConfigureMonorepoDetails", step_result["details"] + ) + configure_monorepo_package( + dest_dir=cfg_mr_pkg_def["package_dir"], + commit_type=commit_type, + hvcs_client_name=hvcs.__class__.__name__.lower(), + hvcs_domain=str(hvcs.hvcs_domain), + tag_format_str=cfg_mr_pkg_def["tag_format_str"], + extra_configs=cfg_mr_pkg_def["extra_configs"], + mask_initial_release=cfg_mr_pkg_def["mask_initial_release"], + package_name=cfg_mr_pkg_def["package_name"], + monorepo=True, + ) + + elif action == RepoActionStep.CHANGE_DIRECTORY: + change_dir_def = cast( + "RepoActionChangeDirectoryDetails", step_result["details"] + ) + if not ( + new_cwd := Path(change_dir_def["directory"]) + .resolve() + .absolute() + ).exists(): + msg = f"Directory {change_dir_def['directory']} does not exist." + raise NotADirectoryError(msg) + + # Helpful Transform to find the project root repo without needing to pass it around (ie '/' => repo_dir) + new_cwd = ( + repo_dir if str(new_cwd) == str(repo_dir.root) else new_cwd + ) + + if not new_cwd.is_dir(): + msg = f"Path {change_dir_def['directory']} is not a directory." + raise NotADirectoryError(msg) + + # TODO: 3.9+, use is_relative_to + # if not new_cwd.is_relative_to(repo_dir): + if repo_dir != new_cwd and repo_dir not in new_cwd.parents: + msg = f"Cannot change directory to '{new_cwd}' as it is outside the repo directory '{repo_dir}'." + raise ValueError(msg) + + os.chdir(str(new_cwd)) + elif action == RepoActionStep.MAKE_COMMITS: mk_cmts_def = cast( "RepoActionMakeCommitsDetails", step_result["details"] @@ -1571,19 +1723,19 @@ def _build_repo_from_definition( # noqa: C901, its required and its just test c ) elif action == RepoActionStep.GIT_MERGE: - this_step = cast("RepoActionGitMerge", step_result) + this_step = cast( + "RepoActionGitMerge[RepoActionGitFFMergeDetails | RepoActionGitMergeDetails]", + step_result, + ) with Repo(repo_dir) as git_repo: if this_step["details"]["fast_forward"]: - ff_merge_def = cast( - "RepoActionGitFFMergeDetails", this_step["details"] + git_repo.git.merge( + this_step["details"]["branch_name"], ff=True ) - git_repo.git.merge(ff_merge_def["branch_name"], ff=True) else: - merge_def = cast( - "RepoActionGitMergeDetails", this_step["details"] - ) + merge_def = this_step["details"] # Update the commit definition with the repo hash merge_def["commit_def"] = create_merge_commit( @@ -1735,7 +1887,14 @@ def _split_repo_actions_by_release_tags( # Loop through all actions and split them by release tags for step in repo_definition: - if any(step["action"] == action for action in [RepoActionStep.CONFIGURE]): + if any( + step["action"] == action + for action in [ + RepoActionStep.CONFIGURE, + RepoActionStep.CREATE_MONOREPO, + RepoActionStep.CONFIGURE_MONOREPO, + ] + ): releasetags_2_steps[None].append(step) continue @@ -1754,6 +1913,7 @@ def _split_repo_actions_by_release_tags( insignificant_actions = [ RepoActionStep.GIT_CHECKOUT, + RepoActionStep.CHANGE_DIRECTORY, ] # Remove Unreleased if there are no significant steps in an Unreleased section diff --git a/tests/fixtures/monorepos/__init__.py b/tests/fixtures/monorepos/__init__.py new file mode 100644 index 000000000..e3546c89f --- /dev/null +++ b/tests/fixtures/monorepos/__init__.py @@ -0,0 +1,4 @@ +from tests.fixtures.monorepos.example_monorepo import * +from tests.fixtures.monorepos.git_monorepo import * +from tests.fixtures.monorepos.github_flow import * +from tests.fixtures.monorepos.trunk_based_dev import * diff --git a/tests/fixtures/monorepos/example_monorepo.py b/tests/fixtures/monorepos/example_monorepo.py new file mode 100644 index 000000000..6150e0635 --- /dev/null +++ b/tests/fixtures/monorepos/example_monorepo.py @@ -0,0 +1,523 @@ +from __future__ import annotations + +from pathlib import Path +from textwrap import dedent +from typing import TYPE_CHECKING + +import pytest + +# NOTE: use backport with newer API +import tests.conftest +import tests.const +import tests.fixtures.example_project +import tests.util +from tests.const import ( + EXAMPLE_PROJECT_NAME, + EXAMPLE_PROJECT_VERSION, + EXAMPLE_PYPROJECT_TOML_CONTENT, + EXAMPLE_RELEASE_NOTES_TEMPLATE, +) +from tests.util import copy_dir_tree, temporary_working_directory + +if TYPE_CHECKING: + from typing import Any, Protocol, Sequence + + from tests.conftest import ( + BuildRepoOrCopyCacheFn, + GetMd5ForSetOfFilesFn, + ) + from tests.fixtures.example_project import ( + UpdatePyprojectTomlFn, + UpdateVersionPyFileFn, + ) + from tests.fixtures.git_repo import RepoActions + + # class GetWheelFileFn(Protocol): + # def __call__(self, version_str: str) -> Path: ... + + class UpdatePkgPyprojectTomlFn(Protocol): + def __call__(self, pkg_name: str, setting: str, value: Any) -> None: ... + + class UseCommonReleaseNotesTemplateFn(Protocol): + def __call__(self) -> None: ... + + +@pytest.fixture(scope="session") +def deps_files_4_example_monorepo() -> list[Path]: + return [ + # This file + Path(__file__).absolute(), + # because of imports + Path(tests.const.__file__).absolute(), + Path(tests.util.__file__).absolute(), + # because of the fixtures + Path(tests.conftest.__file__).absolute(), + Path(tests.fixtures.example_project.__file__).absolute(), + ] + + +@pytest.fixture(scope="session") +def build_spec_hash_4_example_monorepo( + get_md5_for_set_of_files: GetMd5ForSetOfFilesFn, + deps_files_4_example_monorepo: list[Path], +) -> str: + # Generates a hash of the build spec to set when to invalidate the cache + return get_md5_for_set_of_files(deps_files_4_example_monorepo) + + +@pytest.fixture(scope="session") +def cached_example_monorepo( + build_repo_or_copy_cache: BuildRepoOrCopyCacheFn, + monorepo_pkg1_dir: Path, + monorepo_pkg2_dir: Path, + monorepo_pkg1_version_py_file: Path, + monorepo_pkg2_version_py_file: Path, + monorepo_pkg1_pyproject_toml_file: Path, + monorepo_pkg2_pyproject_toml_file: Path, + build_spec_hash_4_example_monorepo: str, + update_version_py_file: UpdateVersionPyFileFn, + update_pyproject_toml: UpdatePyprojectTomlFn, +) -> Path: + """ + Initializes the example monorepo project. DO NOT USE DIRECTLY + + Use the `init_example_monorepo` fixture instead. + """ + + def _build_project(cached_project_path: Path) -> Sequence[RepoActions]: + # purposefully a relative path + # example_dir = version_py_file.parent + gitignore_contents = dedent( + f""" + *.pyc + /{monorepo_pkg1_version_py_file} + /{monorepo_pkg2_version_py_file} + dist/ + """ + ).lstrip() + init_py_contents = dedent( + ''' + """An example package with a very informative docstring.""" + from ._version import __version__ + + def hello_world() -> None: + print("{pkg_name} Hello World") + ''' + ).lstrip() + + with temporary_working_directory(cached_project_path): + update_version_py_file( + version=EXAMPLE_PROJECT_VERSION, + version_file=monorepo_pkg1_version_py_file, + ) + update_version_py_file( + version=EXAMPLE_PROJECT_VERSION, + version_file=monorepo_pkg2_version_py_file, + ) + + file_2_contents: list[tuple[str | Path, str]] = [ + ( + monorepo_pkg1_version_py_file.parent / "__init__.py", + init_py_contents.format(pkg_name="Pkg 1:"), + ), + ( + monorepo_pkg2_version_py_file.parent / "__init__.py", + init_py_contents.format(pkg_name="Pkg 2:"), + ), + (".gitignore", gitignore_contents), + (monorepo_pkg1_pyproject_toml_file, EXAMPLE_PYPROJECT_TOML_CONTENT), + (monorepo_pkg2_pyproject_toml_file, EXAMPLE_PYPROJECT_TOML_CONTENT), + ] + + for file, contents in file_2_contents: + abs_filepath = cached_project_path.joinpath(file).resolve() + # make sure the parent directory exists + abs_filepath.parent.mkdir(parents=True, exist_ok=True) + # write file contents + abs_filepath.write_text(contents) + + config_updates: list[tuple[str, Any, Path]] = [ + ( + "tool.poetry.name", + "pkg-1", + cached_project_path / monorepo_pkg1_pyproject_toml_file, + ), + ( + "tool.poetry.name", + "pkg-2", + cached_project_path / monorepo_pkg2_pyproject_toml_file, + ), + ( + "tool.semantic_release.version_variables", + [ + f"{monorepo_pkg1_version_py_file.relative_to(monorepo_pkg1_dir)}:__version__" + ], + cached_project_path / monorepo_pkg1_pyproject_toml_file, + ), + ( + "tool.semantic_release.version_variables", + [ + f"{monorepo_pkg2_version_py_file.relative_to(monorepo_pkg2_dir)}:__version__" + ], + cached_project_path / monorepo_pkg2_pyproject_toml_file, + ), + ] + + for setting, value, toml_file in config_updates: + update_pyproject_toml( + setting=setting, + value=value, + toml_file=toml_file, + ) + + # This is a special build, we don't expose the Repo Actions to the caller + return [] + + # End of _build_project() + + return build_repo_or_copy_cache( + repo_name="example_monorepo", + build_spec_hash=build_spec_hash_4_example_monorepo, + build_repo_func=_build_project, + ) + + +@pytest.fixture +def init_example_monorepo( + example_project_dir: tests.fixtures.example_project.ExProjectDir, + cached_example_monorepo: Path, + change_to_ex_proj_dir: None, +) -> None: + """This fixture initializes the example project in the current test's project directory.""" + if not cached_example_monorepo.exists(): + raise RuntimeError( + f"Unable to find cached project files for {EXAMPLE_PROJECT_NAME}" + ) + + # Copy the cached project files into the current test's project directory + copy_dir_tree(cached_example_monorepo, example_project_dir) + + +@pytest.fixture +def monorepo_project_w_common_release_notes_template( + init_example_monorepo: None, + monorepo_use_common_release_notes_template: UseCommonReleaseNotesTemplateFn, +) -> None: + monorepo_use_common_release_notes_template() + + +@pytest.fixture(scope="session") +def monorepo_pkg1_name() -> str: + return "pkg1" + + +@pytest.fixture(scope="session") +def monorepo_pkg2_name() -> str: + return "pkg2" + + +@pytest.fixture(scope="session") +def monorepo_pkg_dir_pattern() -> str: + return str(Path("packages", "{package_name}")) + + +@pytest.fixture(scope="session") +def monorepo_pkg1_dir( + monorepo_pkg1_name: str, + monorepo_pkg_dir_pattern: str, +) -> str: + return monorepo_pkg_dir_pattern.format(package_name=monorepo_pkg1_name) + + +@pytest.fixture(scope="session") +def monorepo_pkg2_dir( + monorepo_pkg2_name: str, + monorepo_pkg_dir_pattern: str, +) -> str: + return monorepo_pkg_dir_pattern.format(package_name=monorepo_pkg2_name) + + +@pytest.fixture(scope="session") +def monorepo_pkg_version_py_file_pattern(monorepo_pkg_dir_pattern: str) -> str: + return str(Path(monorepo_pkg_dir_pattern, "src", "{package_name}", "_version.py")) + + +@pytest.fixture(scope="session") +def monorepo_pkg1_version_py_file( + monorepo_pkg1_name: str, + monorepo_pkg_version_py_file_pattern: str, +) -> Path: + return Path( + monorepo_pkg_version_py_file_pattern.format(package_name=monorepo_pkg1_name) + ) + + +@pytest.fixture(scope="session") +def monorepo_pkg2_version_py_file( + monorepo_pkg2_name: str, + monorepo_pkg_version_py_file_pattern: str, +) -> Path: + return Path( + monorepo_pkg_version_py_file_pattern.format(package_name=monorepo_pkg2_name) + ) + + +@pytest.fixture(scope="session") +def monorepo_pkg_pyproject_toml_file_pattern( + monorepo_pkg_dir_pattern: str, + pyproject_toml_file: str, +) -> str: + return str(Path(monorepo_pkg_dir_pattern, pyproject_toml_file)) + + +@pytest.fixture(scope="session") +def monorepo_pkg1_pyproject_toml_file( + monorepo_pkg1_name: str, + monorepo_pkg_pyproject_toml_file_pattern: str, +) -> Path: + return Path( + monorepo_pkg_pyproject_toml_file_pattern.format(package_name=monorepo_pkg1_name) + ) + + +@pytest.fixture(scope="session") +def monorepo_pkg2_pyproject_toml_file( + monorepo_pkg2_name: str, + monorepo_pkg_pyproject_toml_file_pattern: str, +) -> Path: + return Path( + monorepo_pkg_pyproject_toml_file_pattern.format(package_name=monorepo_pkg2_name) + ) + + +@pytest.fixture(scope="session") +def monorepo_pkg_dist_dir_pattern(monorepo_pkg_dir_pattern: str) -> str: + return str(Path(monorepo_pkg_dir_pattern, "dist")) + + +@pytest.fixture(scope="session") +def monorepo_pkg1_dist_dir( + monorepo_pkg1_name: str, + monorepo_pkg_dist_dir_pattern: str, +) -> Path: + return Path(monorepo_pkg_dist_dir_pattern.format(package_name=monorepo_pkg1_name)) + + +@pytest.fixture(scope="session") +def monorepo_pkg2_dist_dir( + monorepo_pkg2_name: str, + monorepo_pkg_dist_dir_pattern: str, +) -> Path: + return Path(monorepo_pkg_dist_dir_pattern.format(package_name=monorepo_pkg2_name)) + + +@pytest.fixture(scope="session") +def monorepo_pkg_changelog_md_file_pattern(monorepo_pkg_dir_pattern: str) -> str: + return str(Path(monorepo_pkg_dir_pattern, "CHANGELOG.md")) + + +@pytest.fixture(scope="session") +def monorepo_pkg1_changelog_md_file( + monorepo_pkg1_name: str, + monorepo_pkg_changelog_md_file_pattern: str, +) -> Path: + return Path( + monorepo_pkg_changelog_md_file_pattern.format(package_name=monorepo_pkg1_name) + ) + + +@pytest.fixture(scope="session") +def monorepo_pkg2_changelog_md_file( + monorepo_pkg2_name: str, + monorepo_pkg_changelog_md_file_pattern: str, +) -> Path: + return Path( + monorepo_pkg_changelog_md_file_pattern.format(package_name=monorepo_pkg2_name) + ) + + +@pytest.fixture(scope="session") +def monorepo_pkg_changelog_rst_file_pattern(monorepo_pkg_dir_pattern: str) -> str: + return str(Path(monorepo_pkg_dir_pattern, "CHANGELOG.rst")) + + +@pytest.fixture(scope="session") +def monorepo_pkg1_changelog_rst_file( + monorepo_pkg1_name: str, + monorepo_pkg_changelog_rst_file_pattern: str, +) -> Path: + return Path( + monorepo_pkg_changelog_rst_file_pattern.format(package_name=monorepo_pkg1_name) + ) + + +@pytest.fixture(scope="session") +def monorepo_pkg2_changelog_rst_file( + monorepo_pkg2_name: str, + monorepo_pkg_changelog_rst_file_pattern: str, +) -> Path: + return Path( + monorepo_pkg_changelog_rst_file_pattern.format(package_name=monorepo_pkg2_name) + ) + + +# @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" + +# return _get_wheel_file + + +@pytest.fixture +def example_monorepo_pkg_dir_pattern( + tmp_path: Path, + monorepo_pkg_dir_pattern: Path, +) -> str: + return str(tmp_path.resolve() / monorepo_pkg_dir_pattern) + + +@pytest.fixture +def example_monorepo_pkg1_dir( + monorepo_pkg1_name: str, + example_monorepo_pkg_dir_pattern: str, +) -> Path: + return Path( + example_monorepo_pkg_dir_pattern.format(package_name=monorepo_pkg1_name) + ) + + +@pytest.fixture +def example_monorepo_pkg2_dir( + monorepo_pkg2_name: str, + example_monorepo_pkg_dir_pattern: str, +) -> Path: + return Path( + example_monorepo_pkg_dir_pattern.format(package_name=monorepo_pkg2_name) + ) + + +@pytest.fixture +def monorepo_use_common_release_notes_template( + example_project_template_dir: Path, + changelog_template_dir: Path, + update_pyproject_toml: UpdatePyprojectTomlFn, + monorepo_pkg1_pyproject_toml_file: Path, + monorepo_pkg2_pyproject_toml_file: Path, +) -> UseCommonReleaseNotesTemplateFn: + config_setting_template_dir = "tool.semantic_release.changelog.template_dir" + + def _use_release_notes_template() -> None: + update_pyproject_toml( + setting=config_setting_template_dir, + value=str( + Path( + *( + "../" + for _ in list(Path(monorepo_pkg1_pyproject_toml_file).parents)[ + :-1 + ] + ), + changelog_template_dir, + ) + ), + toml_file=monorepo_pkg1_pyproject_toml_file, + ) + + update_pyproject_toml( + setting=config_setting_template_dir, + value=str( + Path( + *( + "../" + for _ in list(Path(monorepo_pkg2_pyproject_toml_file).parents)[ + :-1 + ] + ), + changelog_template_dir, + ) + ), + toml_file=monorepo_pkg2_pyproject_toml_file, + ) + + example_project_template_dir.mkdir(parents=True, exist_ok=True) + release_notes_j2 = example_project_template_dir / ".release_notes.md.j2" + release_notes_j2.write_text(EXAMPLE_RELEASE_NOTES_TEMPLATE) + + return _use_release_notes_template + + +# @pytest.fixture +# def example_pyproject_toml( +# example_project_dir: ExProjectDir, +# pyproject_toml_file: Path, +# ) -> Path: +# return example_project_dir / pyproject_toml_file + + +# @pytest.fixture +# def example_dist_dir( +# example_project_dir: ExProjectDir, +# dist_dir: Path, +# ) -> Path: +# return example_project_dir / dist_dir + + +# @pytest.fixture +# def example_project_wheel_file( +# example_dist_dir: Path, +# get_wheel_file: GetWheelFileFn, +# ) -> Path: +# return example_dist_dir / get_wheel_file(EXAMPLE_PROJECT_VERSION) + + +# Note this is just the path and the content may change +# @pytest.fixture +# def example_changelog_md( +# example_project_dir: ExProjectDir, +# changelog_md_file: Path, +# ) -> Path: +# return example_project_dir / changelog_md_file + + +# Note this is just the path and the content may change +# @pytest.fixture +# def example_changelog_rst( +# example_project_dir: ExProjectDir, +# changelog_rst_file: Path, +# ) -> Path: +# return example_project_dir / changelog_rst_file + + +# @pytest.fixture +# def example_project_template_dir( +# example_project_dir: ExProjectDir, +# changelog_template_dir: Path, +# ) -> Path: +# return example_project_dir / changelog_template_dir + + +@pytest.fixture(scope="session") +def update_pkg_pyproject_toml( + update_pyproject_toml: UpdatePyprojectTomlFn, + monorepo_pkg_pyproject_toml_file_pattern: str, +) -> UpdatePkgPyprojectTomlFn: + """Update the pyproject.toml file with the given content.""" + + def _update_pyproject_toml(pkg_name: str, setting: str, value: Any) -> None: + toml_file = Path( + monorepo_pkg_pyproject_toml_file_pattern.format(package_name=pkg_name) + ).resolve() + + if not toml_file.exists(): + raise FileNotFoundError( + f"pyproject.toml file for package {pkg_name} not found at {toml_file}" + ) + + update_pyproject_toml( + setting=setting, + value=value, + toml_file=toml_file, + ) + + return _update_pyproject_toml diff --git a/tests/fixtures/monorepos/git_monorepo.py b/tests/fixtures/monorepos/git_monorepo.py new file mode 100644 index 000000000..c88fbdb29 --- /dev/null +++ b/tests/fixtures/monorepos/git_monorepo.py @@ -0,0 +1,206 @@ +from __future__ import annotations + +from pathlib import Path +from shutil import rmtree +from typing import TYPE_CHECKING + +import pytest +from git import Repo + +import tests.conftest +import tests.const +import tests.fixtures.git_repo +import tests.util +from tests.const import ( + DEFAULT_BRANCH_NAME, + EXAMPLE_HVCS_DOMAIN, + EXAMPLE_PROJECT_NAME, +) +from tests.util import copy_dir_tree + +if TYPE_CHECKING: + from typing import Protocol, Sequence + + from git import Actor + + from semantic_release.hvcs import HvcsBase + + from tests.conftest import ( + BuildRepoOrCopyCacheFn, + GetMd5ForSetOfFilesFn, + RepoActions, + ) + from tests.fixtures.git_repo import ( + BuildRepoFn, + CommitConvention, + TomlSerializableTypes, + ) + + class BuildMonorepoFn(Protocol): + def __call__(self, dest_dir: Path | str) -> Path: ... + + +@pytest.fixture(scope="session") +def deps_files_4_example_git_monorepo( + deps_files_4_example_monorepo: list[Path], +) -> list[Path]: + return [ + *deps_files_4_example_monorepo, + # This file + Path(__file__).absolute(), + # because of imports + Path(tests.const.__file__).absolute(), + Path(tests.util.__file__).absolute(), + # because of the fixtures + Path(tests.conftest.__file__).absolute(), + Path(tests.fixtures.git_repo.__file__).absolute(), + ] + + +@pytest.fixture(scope="session") +def build_spec_hash_4_example_git_monorepo( + get_md5_for_set_of_files: GetMd5ForSetOfFilesFn, + deps_files_4_example_git_monorepo: list[Path], +) -> str: + # Generates a hash of the build spec to set when to invalidate the cache + return get_md5_for_set_of_files(deps_files_4_example_git_monorepo) + + +@pytest.fixture(scope="session") +def cached_example_git_monorepo( + build_repo_or_copy_cache: BuildRepoOrCopyCacheFn, + build_spec_hash_4_example_git_monorepo: str, + cached_example_monorepo: Path, + example_git_https_url: str, + commit_author: Actor, +) -> Path: + """ + Initializes an example monorepo project with git. DO NOT USE DIRECTLY. + + Use a `repo_*` fixture instead. This creates a default + base repository, all settings can be changed later through from the + example_project_git_repo fixture's return object and manual adjustment. + """ + + def _build_repo(cached_repo_path: Path) -> Sequence[RepoActions]: + if not cached_example_monorepo.exists(): + raise RuntimeError("Unable to find cached monorepo files") + + # make a copy of the example monorepo as a base + copy_dir_tree(cached_example_monorepo, cached_repo_path) + + # initialize git repo (open and close) + # 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: + config.set_value("user", "name", commit_author.name) + config.set_value("user", "email", commit_author.email) + config.set_value("commit", "gpgsign", False) + config.set_value("tag", "gpgsign", False) + + repo.create_remote(name="origin", url=example_git_https_url) + + # make sure all base files are in index to enable initial commit + repo.index.add(("*", ".gitignore")) + + # This is a special build, we don't expose the Repo Actions to the caller + return [] + + # End of _build_repo() + + return build_repo_or_copy_cache( + repo_name=cached_example_git_monorepo.__name__.split("_", maxsplit=1)[1], + build_spec_hash=build_spec_hash_4_example_git_monorepo, + build_repo_func=_build_repo, + ) + + +@pytest.fixture(scope="session") +def file_in_pkg_pattern(file_in_repo: str, monorepo_pkg_dir_pattern: str) -> str: + return str(Path(monorepo_pkg_dir_pattern) / file_in_repo) + + +@pytest.fixture(scope="session") +def file_in_monorepo_pkg1( + monorepo_pkg1_name: str, + file_in_pkg_pattern: str, +) -> Path: + return Path(file_in_pkg_pattern.format(pkg_name=monorepo_pkg1_name)) + + +@pytest.fixture(scope="session") +def file_in_monorepo_pkg2( + monorepo_pkg2_name: str, + file_in_pkg_pattern: str, +) -> Path: + return Path(file_in_pkg_pattern.format(pkg_name=monorepo_pkg2_name)) + + +@pytest.fixture(scope="session") +def build_base_monorepo( # noqa: C901 + cached_example_git_monorepo: Path, +) -> BuildMonorepoFn: + """ + 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_monorepo(dest_dir: Path | str) -> Path: + if not cached_example_git_monorepo.exists(): + raise RuntimeError("Unable to find cached git project files!") + + # Copy the cached git project the dest directory + copy_dir_tree(cached_example_git_monorepo, dest_dir) + + return Path(dest_dir) + + return _build_configured_base_monorepo + + +@pytest.fixture(scope="session") +def configure_monorepo_package( # noqa: C901 + 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 _configure( # 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, + monorepo: bool = True, + ) -> tuple[Path, HvcsBase]: + if not monorepo: + raise ValueError("This fixture is only for monorepo packages!") + + if not Path(dest_dir).exists(): + raise RuntimeError(f"Destination directory {dest_dir} does not exist!") + + 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, + monorepo=monorepo, + ) + + return _configure diff --git a/tests/fixtures/monorepos/github_flow/__init__.py b/tests/fixtures/monorepos/github_flow/__init__.py new file mode 100644 index 000000000..3b951b378 --- /dev/null +++ b/tests/fixtures/monorepos/github_flow/__init__.py @@ -0,0 +1,2 @@ +from tests.fixtures.monorepos.github_flow.monorepo_w_default_release import * +from tests.fixtures.monorepos.github_flow.monorepo_w_release_channels import * diff --git a/tests/fixtures/monorepos/github_flow/monorepo_w_default_release.py b/tests/fixtures/monorepos/github_flow/monorepo_w_default_release.py new file mode 100644 index 000000000..f42789953 --- /dev/null +++ b/tests/fixtures/monorepos/github_flow/monorepo_w_default_release.py @@ -0,0 +1,954 @@ +from __future__ import annotations + +from datetime import timedelta +from itertools import count +from pathlib import Path +from textwrap import dedent +from typing import TYPE_CHECKING, cast + +import pytest + +from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.commit_parser.conventional.options_monorepo import ( + ConventionalCommitMonorepoParserOptions, +) +from semantic_release.commit_parser.conventional.parser_monorepo import ( + ConventionalCommitMonorepoParser, +) +from semantic_release.version.version import Version + +import tests.conftest +import tests.const +import tests.util +from tests.const import ( + DEFAULT_BRANCH_NAME, + EXAMPLE_HVCS_DOMAIN, + INITIAL_COMMIT_MESSAGE, + RepoActionStep, +) + +if TYPE_CHECKING: + from typing import Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.token import ParseResult + + from tests.conftest import ( + GetCachedRepoDataFn, + GetMd5ForSetOfFilesFn, + GetStableDateNowFn, + ) + from tests.fixtures.example_project import ExProjectDir + from tests.fixtures.git_repo import ( + BuildRepoFromDefinitionFn, + BuildRepoOrCopyCacheFn, + BuildSpecificRepoFn, + BuiltRepoResult, + CommitConvention, + CommitSpec, + ConvertCommitSpecsToCommitDefsFn, + ConvertCommitSpecToCommitDefFn, + ExProjectGitRepoFn, + FormatGitHubSquashCommitMsgFn, + GetRepoDefinitionFn, + RepoActionChangeDirectory, + RepoActions, + RepoActionWriteChangelogsDestFile, + TomlSerializableTypes, + ) + + +@pytest.fixture(scope="session") +def deps_files_4_github_flow_monorepo_w_default_release_channel( + deps_files_4_example_git_monorepo: list[Path], +) -> list[Path]: + return [ + *deps_files_4_example_git_monorepo, + # This file + Path(__file__).absolute(), + # because of imports + Path(tests.const.__file__).absolute(), + Path(tests.util.__file__).absolute(), + # because of the fixtures + Path(tests.conftest.__file__).absolute(), + ] + + +@pytest.fixture(scope="session") +def build_spec_hash_4_github_flow_monorepo_w_default_release_channel( + get_md5_for_set_of_files: GetMd5ForSetOfFilesFn, + deps_files_4_github_flow_monorepo_w_default_release_channel: list[Path], +) -> str: + # Generates a hash of the build spec to set when to invalidate the cache + return get_md5_for_set_of_files( + deps_files_4_github_flow_monorepo_w_default_release_channel + ) + + +@pytest.fixture(scope="session") +def get_repo_definition_4_github_flow_monorepo_w_default_release_channel( + convert_commit_specs_to_commit_defs: ConvertCommitSpecsToCommitDefsFn, + convert_commit_spec_to_commit_def: ConvertCommitSpecToCommitDefFn, + format_squash_commit_msg_github: FormatGitHubSquashCommitMsgFn, + monorepo_pkg1_changelog_md_file: Path, + monorepo_pkg1_changelog_rst_file: Path, + monorepo_pkg2_changelog_md_file: Path, + monorepo_pkg2_changelog_rst_file: Path, + monorepo_pkg1_name: str, + monorepo_pkg2_name: str, + monorepo_pkg1_dir: Path, + monorepo_pkg2_dir: Path, + monorepo_pkg1_version_py_file: Path, + monorepo_pkg2_version_py_file: Path, + monorepo_pkg1_pyproject_toml_file: Path, + monorepo_pkg2_pyproject_toml_file: Path, + stable_now_date: GetStableDateNowFn, + default_tag_format_str: str, +) -> GetRepoDefinitionFn: + """ + Builds a Monorepo with the GitHub Flow branching strategy and a squash commit merging strategy + for a single release channel on the default branch. + + Implementation: + - The monorepo contains two packages, each with its own internal changelog but shared template. + - The repository implements the following git graph: + + ``` + * chore(release): pkg1@1.1.0 [skip ci] (tag: pkg1-v1.1.0, branch: main, HEAD -> main) + * feat(pkg1): file modified outside of pkg 1, identified by scope (#5) + | + | * feat(pkg1): file modified outside of pkg 1, identified by scope (branch: pkg1/feat/pr-4) + |/ + * chore(release): pkg2@1.1.1 [skip ci] (tag: pkg2-v1.1.1) + * fix(pkg2-cli): file modified outside of pkg 2, identified by scope (#4) + | + | * fix(pkg2-cli): file modified outside of pkg 2, identified by scope (branch: pkg2/fix/pr-3) + |/ + * chore(release): pkg2@1.1.0 [skip ci] (tag: pkg2-v1.1.0) + * feat: no pkg scope but file in pkg 2 directory (#3) # Squash merge of pkg2/feat/pr-2 + * chore(release): pkg1@1.0.1 [skip ci] (tag: pkg1-v1.0.1) + * fix: no pkg scope but file in pkg 1 directory (#2) # Squash merge of pkg1/fix/pr-1 + | + | * docs(cli): add cli documentation + | * test(cli): add cli tests + | * feat: no pkg scope but file in pkg 2 directory (branch: pkg2/feat/pr-2) + |/ + | * fix: no pkg scope but file in pkg 1 directory (branch: pkg1/fix/pr-1) + |/ + * chore(release): pkg2@1.0.0 [skip ci] (tag: pkg2-v1.0.0) # Initial release of pkg 2 + * chore(release): pkg1@1.0.0 [skip ci] (tag: pkg1-v1.0.0) # Initial release of pkg 1 + * Initial commit # Includes core functionality for both packages + ``` + """ + + def _get_repo_from_definition( + commit_type: CommitConvention, + hvcs_client_name: str = "github", + hvcs_domain: str = EXAMPLE_HVCS_DOMAIN, + tag_format_str: str | None = default_tag_format_str, + extra_configs: dict[str, TomlSerializableTypes] | None = None, + mask_initial_release: bool = True, + ignore_merge_commits: bool = True, + ) -> Sequence[RepoActions]: + stable_now_datetime = stable_now_date() + commit_timestamp_gen = ( + (stable_now_datetime + timedelta(seconds=i)).isoformat(timespec="seconds") + for i in count(step=1) + ) + pr_num_gen = (i for i in count(start=2, step=1)) + + pkg1_changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ + { + "path": monorepo_pkg1_changelog_md_file, + "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": True, + }, + { + "path": monorepo_pkg1_changelog_rst_file, + "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": True, + }, + ] + + pkg2_changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ + { + "path": monorepo_pkg2_changelog_md_file, + "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": True, + }, + { + "path": monorepo_pkg2_changelog_rst_file, + "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": True, + }, + ] + + change_to_pkg1_dir: RepoActionChangeDirectory = { + "action": RepoActionStep.CHANGE_DIRECTORY, + "details": { + "directory": monorepo_pkg1_dir, + }, + } + + change_to_pkg2_dir: RepoActionChangeDirectory = { + "action": RepoActionStep.CHANGE_DIRECTORY, + "details": { + "directory": monorepo_pkg2_dir, + }, + } + + change_to_example_project_dir: RepoActionChangeDirectory = { + "action": RepoActionStep.CHANGE_DIRECTORY, + "details": { + "directory": "/", + }, + } + + if commit_type != "conventional": + raise ValueError(f"Unsupported commit type: {commit_type}") + + pkg1_commit_parser = ConventionalCommitMonorepoParser( + options=ConventionalCommitMonorepoParserOptions( + parse_squash_commits=True, + ignore_merge_commits=ignore_merge_commits, + scope_prefix=f"{monorepo_pkg1_name}-?", + path_filters=(".",), + ) + ) + + pkg2_commit_parser = ConventionalCommitMonorepoParser( + options=ConventionalCommitMonorepoParserOptions( + parse_squash_commits=pkg1_commit_parser.options.parse_squash_commits, + ignore_merge_commits=pkg1_commit_parser.options.ignore_merge_commits, + scope_prefix=f"{monorepo_pkg2_name}-?", + path_filters=(".",), + ) + ) + + common_configs: dict[str, TomlSerializableTypes] = { + # Set the default release branch + "tool.semantic_release.branches.main": { + "match": r"^(main|master)$", + "prerelease": False, + }, + "tool.semantic_release.allow_zero_version": False, + "tool.semantic_release.changelog.exclude_commit_patterns": [r"^chore"], + "tool.semantic_release.commit_parser": f"{commit_type}-monorepo", + "tool.semantic_release.commit_parser_options.parse_squash_commits": pkg1_commit_parser.options.parse_squash_commits, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": pkg1_commit_parser.options.ignore_merge_commits, + } + + mr1_pkg1_fix_branch_name = f"{monorepo_pkg1_name}/fix/pr-1" + mr2_pkg2_feat_branch_name = f"{monorepo_pkg2_name}/feat/pr-2" + mr3_pkg2_fix_branch_name = f"{monorepo_pkg2_name}/fix/pr-3" + mr4_pkg1_feat_branch_name = f"{monorepo_pkg1_name}/feat/pr-4" + + pkg1_new_version = Version.parse( + "1.0.0", tag_format=f"{monorepo_pkg1_name}-{tag_format_str}" + ) + pkg2_new_version = Version.parse( + "1.0.0", tag_format=f"{monorepo_pkg2_name}-{tag_format_str}" + ) + + repo_construction_steps: list[RepoActions] = [ + { + "action": RepoActionStep.CREATE_MONOREPO, + "details": { + "commit_type": commit_type, + "hvcs_client_name": hvcs_client_name, + "hvcs_domain": hvcs_domain, + "post_actions": [ + { + "action": RepoActionStep.CONFIGURE_MONOREPO, + "details": { + "package_dir": monorepo_pkg1_dir, + "package_name": monorepo_pkg1_name, + "tag_format_str": pkg1_new_version.tag_format, + "mask_initial_release": mask_initial_release, + "extra_configs": { + **common_configs, + "tool.semantic_release.commit_message": ( + pkg1_cmt_msg_format := dedent( + f"""\ + chore(release): {monorepo_pkg1_name}@{{version}} [skip ci] + + Automatically generated by python-semantic-release + """ + ) + ), + "tool.semantic_release.commit_parser_options.scope_prefix": pkg1_commit_parser.options.scope_prefix, + "tool.semantic_release.commit_parser_options.path_filters": pkg1_commit_parser.options.path_filters, + **(extra_configs or {}), + }, + }, + }, + { + "action": RepoActionStep.CONFIGURE_MONOREPO, + "details": { + "package_dir": monorepo_pkg2_dir, + "package_name": monorepo_pkg2_name, + "tag_format_str": pkg2_new_version.tag_format, + "mask_initial_release": mask_initial_release, + "extra_configs": { + **common_configs, + "tool.semantic_release.commit_message": ( + pkg2_cmt_msg_format := dedent( + f"""\ + chore(release): {monorepo_pkg2_name}@{{version}} [skip ci] + + Automatically generated by python-semantic-release + """ + ) + ), + "tool.semantic_release.commit_parser_options.scope_prefix": pkg2_commit_parser.options.scope_prefix, + "tool.semantic_release.commit_parser_options.path_filters": pkg2_commit_parser.options.path_filters, + **(extra_configs or {}), + }, + }, + }, + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "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, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": bool( + commit_type == "emoji" + ), + }, + ], + commit_type, + # this parser does not matter since the commit is common + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg1_commit_parser, + ), + monorepo=True, + ), + }, + }, + ], + }, + } + ] + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg1_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg1_new_version.tag_format, + "version_py_file": monorepo_pkg1_version_py_file.relative_to( + monorepo_pkg1_dir + ), + "commit_message_format": pkg1_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg1_new_version, + "dest_files": pkg1_changelog_file_definitions, + "commit_ids": [cid_c1_initial], + }, + }, + change_to_pkg1_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg2_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg2_new_version.tag_format, + "version_py_file": monorepo_pkg2_version_py_file.relative_to( + monorepo_pkg2_dir + ), + "commit_message_format": pkg2_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg2_new_version, + "dest_files": pkg2_changelog_file_definitions, + "commit_ids": [cid_c1_initial], + }, + }, + change_to_pkg2_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + pkg1_fix_branch_commits: Sequence[CommitSpec] = [ + { + "cid": "pkg1-fix-1-squashed", + "conventional": "fix: no pkg scope but file in pkg 1 directory\n\nResolves: #123\n", + "emoji": ":bug: no pkg scope but file in pkg 1 directory\n\nResolves: #123\n", + "scipy": "MAINT: no pkg scope but file in pkg 1 directory\n\nResolves: #123\n", + "datetime": next(commit_timestamp_gen), + }, + ] + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": { + "create_branch": { + "name": mr1_pkg1_fix_branch_name, + "start_branch": DEFAULT_BRANCH_NAME, + } + }, + }, + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "pre_actions": [change_to_pkg1_dir], + "commits": convert_commit_specs_to_commit_defs( + [ + { + **commit, + "include_in_changelog": False, + } + for commit in pkg1_fix_branch_commits + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg1_commit_parser, + ), + monorepo=True, + ), + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + # simulate separate work by another person at same time as the fix branch + pkg2_feat_branch_commits: Sequence[CommitSpec] = [ + { + "cid": "pkg2-feat-1-squashed", + "conventional": "feat: no pkg scope but file in pkg 2 directory", + "emoji": ":sparkles: no pkg scope but file in pkg 2 directory", + "scipy": "ENH: no pkg scope but file in pkg 2 directory", + "datetime": next(commit_timestamp_gen), + }, + { + "cid": "pkg2-feat-2-squashed", + "conventional": "test(cli): add cli tests", + "emoji": ":checkmark: add cli tests", + "scipy": "TST: add cli tests", + "datetime": next(commit_timestamp_gen), + }, + { + "cid": "pkg2-feat-3-squashed", + "conventional": "docs(cli): add cli documentation", + "emoji": ":memo: add cli documentation", + "scipy": "DOC: add cli documentation", + "datetime": next(commit_timestamp_gen), + }, + ] + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": { + "create_branch": { + "name": mr2_pkg2_feat_branch_name, + "start_branch": DEFAULT_BRANCH_NAME, + }, + }, + }, + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "pre_actions": [change_to_pkg2_dir], + "commits": convert_commit_specs_to_commit_defs( + [ + { + **commit, + "include_in_changelog": False, + } + for commit in pkg2_feat_branch_commits + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg2_commit_parser, + ), + monorepo=True, + ), + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + pkg1_new_version = Version.parse( + "1.0.1", tag_format=pkg1_new_version.tag_format + ) + + all_commit_types: list[CommitConvention] = ["conventional", "emoji", "scipy"] + fix_branch_pr_number = next(pr_num_gen) + fix_branch_squash_commit_spec: CommitSpec = { + "cid": "mr1-pkg1-fix", + **{ # type: ignore[typeddict-item] + cmt_type: format_squash_commit_msg_github( + # Use the primary commit message as the PR title + pr_title=pkg1_fix_branch_commits[0][cmt_type], + pr_number=fix_branch_pr_number, + squashed_commits=[ + cmt[commit_type] for cmt in pkg1_fix_branch_commits[1:] + ], + ) + for cmt_type in all_commit_types + }, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + } + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": {"branch": DEFAULT_BRANCH_NAME}, + }, + { + "action": RepoActionStep.GIT_SQUASH, + "details": { + "branch": mr1_pkg1_fix_branch_name, + "strategy_option": "theirs", + "commit_def": convert_commit_spec_to_commit_def( + fix_branch_squash_commit_spec, + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg1_commit_parser, + ), + monorepo=True, + ), + "config_file": monorepo_pkg1_pyproject_toml_file, + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg1_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg1_new_version.tag_format, + "version_py_file": monorepo_pkg1_version_py_file.relative_to( + monorepo_pkg1_dir + ), + "commit_message_format": pkg1_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg1_new_version, + "dest_files": pkg1_changelog_file_definitions, + "commit_ids": [ + f'{fix_branch_squash_commit_spec["cid"]}-{index + 1}' + for index in range(len(pkg1_fix_branch_commits)) + ], + }, + }, + change_to_pkg1_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + feat_branch_pr_number = next(pr_num_gen) + feat_branch_squash_commit_spec: CommitSpec = { + "cid": "mr2-pkg2-feat", + **{ # type: ignore[typeddict-item] + cmt_type: format_squash_commit_msg_github( + # Use the primary commit message as the PR title + pr_title=pkg2_feat_branch_commits[0][cmt_type], + pr_number=feat_branch_pr_number, + squashed_commits=[ + cmt[commit_type] for cmt in pkg2_feat_branch_commits[1:] + ], + ) + for cmt_type in all_commit_types + }, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + } + + pkg2_new_version = Version.parse( + "1.1.0", tag_format=pkg2_new_version.tag_format + ) + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_SQUASH, + "details": { + "branch": mr2_pkg2_feat_branch_name, + "strategy_option": "theirs", + "commit_def": convert_commit_spec_to_commit_def( + feat_branch_squash_commit_spec, + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg2_commit_parser, + ), + monorepo=True, + ), + "config_file": monorepo_pkg2_pyproject_toml_file, + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg2_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg2_new_version.tag_format, + "version_py_file": monorepo_pkg2_version_py_file.relative_to( + monorepo_pkg2_dir + ), + "commit_message_format": pkg2_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg2_new_version, + "dest_files": pkg2_changelog_file_definitions, + "commit_ids": [ + f'{feat_branch_squash_commit_spec["cid"]}-{index + 1}' + for index in range( + len(pkg2_feat_branch_commits) + ) + ], + }, + }, + change_to_pkg2_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + pkg2_fix_branch_commits: Sequence[CommitSpec] = [ + { + "cid": "pkg2-fix-1-squashed", + "conventional": "fix(pkg2-cli): file modified outside of pkg 2, identified by scope\n\nResolves: #123\n", + "emoji": ":bug: (pkg2-cli) file modified outside of pkg 2, identified by scope\n\nResolves: #123\n", + "scipy": "MAINT:pkg2-cli: file modified outside of pkg 2, identified by scope\n\nResolves: #123\n", + "datetime": next(commit_timestamp_gen), + }, + ] + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": { + "create_branch": { + "name": mr3_pkg2_fix_branch_name, + "start_branch": DEFAULT_BRANCH_NAME, + } + }, + }, + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "commits": convert_commit_specs_to_commit_defs( + [ + { + **commit, + "include_in_changelog": False, + } + for commit in pkg2_fix_branch_commits + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg2_commit_parser, + ), + monorepo=True, + ), + }, + }, + ] + ) + + pkg2_new_version = Version.parse( + "1.1.1", tag_format=pkg2_new_version.tag_format + ) + + fix_branch_pr_number = next(pr_num_gen) + fix_branch_squash_commit_spec = { + "cid": "mr3-pkg2-fix", + **{ # type: ignore[typeddict-item] + cmt_type: format_squash_commit_msg_github( + # Use the primary commit message as the PR title + pr_title=pkg2_fix_branch_commits[0][cmt_type], + pr_number=fix_branch_pr_number, + squashed_commits=[ + cmt[commit_type] for cmt in pkg2_fix_branch_commits[1:] + ], + ) + for cmt_type in all_commit_types + }, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + } + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": {"branch": DEFAULT_BRANCH_NAME}, + }, + { + "action": RepoActionStep.GIT_SQUASH, + "details": { + "branch": mr3_pkg2_fix_branch_name, + "strategy_option": "theirs", + "commit_def": convert_commit_spec_to_commit_def( + fix_branch_squash_commit_spec, + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg2_commit_parser, + ), + monorepo=True, + ), + "config_file": monorepo_pkg2_pyproject_toml_file, + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg2_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg2_new_version.tag_format, + "version_py_file": monorepo_pkg2_version_py_file.relative_to( + monorepo_pkg2_dir + ), + "commit_message_format": pkg2_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg2_new_version, + "dest_files": pkg2_changelog_file_definitions, + "commit_ids": [ + f'{fix_branch_squash_commit_spec["cid"]}-{index + 1}' + for index in range(len(pkg2_fix_branch_commits)) + ], + }, + }, + change_to_pkg2_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + pkg1_feat_branch_commits: Sequence[CommitSpec] = [ + { + "cid": "pkg1-feat-1-squashed", + "conventional": "feat(pkg1): file modified outside of pkg 1, identified by scope", + "emoji": ":sparkles: (pkg1) file modified outside of pkg 1, identified by scope", + "scipy": "ENH:pkg1: file modified outside of pkg 1, identified by scope", + "datetime": next(commit_timestamp_gen), + } + ] + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": { + "create_branch": { + "name": mr4_pkg1_feat_branch_name, + "start_branch": DEFAULT_BRANCH_NAME, + }, + }, + }, + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "commits": convert_commit_specs_to_commit_defs( + [ + { + **commit, + "include_in_changelog": False, + } + for commit in pkg1_feat_branch_commits + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg1_commit_parser, + ), + monorepo=True, + ), + }, + }, + ] + ) + + feat_branch_pr_number = next(pr_num_gen) + feat_branch_squash_commit_spec = { + "cid": "mr4-pkg1-feat", + **{ # type: ignore[typeddict-item] + cmt_type: format_squash_commit_msg_github( + # Use the primary commit message as the PR title + pr_title=pkg1_feat_branch_commits[0][cmt_type], + pr_number=feat_branch_pr_number, + squashed_commits=[ + cmt[commit_type] for cmt in pkg1_feat_branch_commits[1:] + ], + ) + for cmt_type in all_commit_types + }, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + } + + pkg1_new_version = Version.parse( + "1.1.0", tag_format=pkg1_new_version.tag_format + ) + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": {"branch": DEFAULT_BRANCH_NAME}, + }, + { + "action": RepoActionStep.GIT_SQUASH, + "details": { + "branch": mr4_pkg1_feat_branch_name, + "strategy_option": "theirs", + "commit_def": convert_commit_spec_to_commit_def( + feat_branch_squash_commit_spec, + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg1_commit_parser, + ), + monorepo=True, + ), + "config_file": monorepo_pkg1_pyproject_toml_file, + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg1_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg1_new_version.tag_format, + "version_py_file": monorepo_pkg1_version_py_file.relative_to( + monorepo_pkg1_dir + ), + "commit_message_format": pkg1_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg1_new_version, + "dest_files": pkg1_changelog_file_definitions, + "commit_ids": [ + f'{feat_branch_squash_commit_spec["cid"]}-{index + 1}' + for index in range( + len(pkg1_feat_branch_commits) + ) + ], + }, + }, + change_to_pkg1_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + return repo_construction_steps + + return _get_repo_from_definition + + +@pytest.fixture(scope="session") +def build_monorepo_w_github_flow_w_default_release_channel( + build_repo_from_definition: BuildRepoFromDefinitionFn, + get_repo_definition_4_github_flow_monorepo_w_default_release_channel: GetRepoDefinitionFn, + get_cached_repo_data: GetCachedRepoDataFn, + build_repo_or_copy_cache: BuildRepoOrCopyCacheFn, + build_spec_hash_4_github_flow_monorepo_w_default_release_channel: str, +) -> BuildSpecificRepoFn: + def _build_specific_repo_type( + repo_name: str, commit_type: CommitConvention, dest_dir: Path + ) -> Sequence[RepoActions]: + def _build_repo(cached_repo_path: Path) -> Sequence[RepoActions]: + repo_construction_steps = ( + get_repo_definition_4_github_flow_monorepo_w_default_release_channel( + commit_type=commit_type, + ) + ) + return build_repo_from_definition(cached_repo_path, repo_construction_steps) + + build_repo_or_copy_cache( + repo_name=repo_name, + build_spec_hash=build_spec_hash_4_github_flow_monorepo_w_default_release_channel, + build_repo_func=_build_repo, + dest_dir=dest_dir, + ) + + if not (cached_repo_data := get_cached_repo_data(proj_dirname=repo_name)): + raise ValueError("Failed to retrieve repo data from cache") + + return cached_repo_data["build_definition"] + + return _build_specific_repo_type + + +# --------------------------------------------------------------------------- # +# Test-level fixtures that will cache the built directory & set up test case # +# --------------------------------------------------------------------------- # + + +@pytest.fixture +def monorepo_w_github_flow_w_default_release_channel_conventional_commits( + build_monorepo_w_github_flow_w_default_release_channel: BuildSpecificRepoFn, + example_project_git_repo: ExProjectGitRepoFn, + example_project_dir: ExProjectDir, + change_to_ex_proj_dir: None, +) -> BuiltRepoResult: + repo_name = ( + monorepo_w_github_flow_w_default_release_channel_conventional_commits.__name__ + ) + commit_type: CommitConvention = repo_name.split("_")[-2] # type: ignore[assignment] + + return { + "definition": build_monorepo_w_github_flow_w_default_release_channel( + repo_name=repo_name, + commit_type=commit_type, + dest_dir=example_project_dir, + ), + "repo": example_project_git_repo(), + } diff --git a/tests/fixtures/monorepos/github_flow/monorepo_w_release_channels.py b/tests/fixtures/monorepos/github_flow/monorepo_w_release_channels.py new file mode 100644 index 000000000..1e25cdb4f --- /dev/null +++ b/tests/fixtures/monorepos/github_flow/monorepo_w_release_channels.py @@ -0,0 +1,888 @@ +from __future__ import annotations + +from datetime import timedelta +from itertools import count +from pathlib import Path +from textwrap import dedent +from typing import TYPE_CHECKING, cast + +import pytest + +from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.commit_parser.conventional.options_monorepo import ( + ConventionalCommitMonorepoParserOptions, +) +from semantic_release.commit_parser.conventional.parser_monorepo import ( + ConventionalCommitMonorepoParser, +) +from semantic_release.version.version import Version + +import tests.conftest +import tests.const +import tests.util +from tests.const import ( + DEFAULT_BRANCH_NAME, + EXAMPLE_HVCS_DOMAIN, + INITIAL_COMMIT_MESSAGE, + RepoActionStep, +) + +if TYPE_CHECKING: + from typing import Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.token import ParseResult + + from tests.conftest import ( + GetCachedRepoDataFn, + GetMd5ForSetOfFilesFn, + GetStableDateNowFn, + ) + from tests.fixtures.example_project import ExProjectDir + from tests.fixtures.git_repo import ( + BuildRepoFromDefinitionFn, + BuildRepoOrCopyCacheFn, + BuildSpecificRepoFn, + BuiltRepoResult, + CommitConvention, + ConvertCommitSpecsToCommitDefsFn, + ConvertCommitSpecToCommitDefFn, + ExProjectGitRepoFn, + FormatGitHubMergeCommitMsgFn, + GetRepoDefinitionFn, + RepoActionChangeDirectory, + RepoActionGitMerge, + RepoActionGitMergeDetails, + RepoActions, + RepoActionWriteChangelogsDestFile, + TomlSerializableTypes, + ) + + +@pytest.fixture(scope="session") +def deps_files_4_github_flow_monorepo_w_feature_release_channel( + deps_files_4_example_git_monorepo: list[Path], +) -> list[Path]: + return [ + *deps_files_4_example_git_monorepo, + # This file + Path(__file__).absolute(), + # because of imports + Path(tests.const.__file__).absolute(), + Path(tests.util.__file__).absolute(), + # because of the fixtures + Path(tests.conftest.__file__).absolute(), + ] + + +@pytest.fixture(scope="session") +def build_spec_hash_4_github_flow_monorepo_w_feature_release_channel( + get_md5_for_set_of_files: GetMd5ForSetOfFilesFn, + deps_files_4_github_flow_monorepo_w_feature_release_channel: list[Path], +) -> str: + # Generates a hash of the build spec to set when to invalidate the cache + return get_md5_for_set_of_files( + deps_files_4_github_flow_monorepo_w_feature_release_channel + ) + + +@pytest.fixture(scope="session") +def get_repo_definition_4_github_flow_monorepo_w_feature_release_channel( + convert_commit_specs_to_commit_defs: ConvertCommitSpecsToCommitDefsFn, + convert_commit_spec_to_commit_def: ConvertCommitSpecToCommitDefFn, + format_merge_commit_msg_github: FormatGitHubMergeCommitMsgFn, + monorepo_pkg1_changelog_md_file: Path, + monorepo_pkg1_changelog_rst_file: Path, + monorepo_pkg2_changelog_md_file: Path, + monorepo_pkg2_changelog_rst_file: Path, + monorepo_pkg1_name: str, + monorepo_pkg2_name: str, + monorepo_pkg1_dir: Path, + monorepo_pkg2_dir: Path, + monorepo_pkg1_version_py_file: Path, + monorepo_pkg2_version_py_file: Path, + stable_now_date: GetStableDateNowFn, + default_tag_format_str: str, +) -> GetRepoDefinitionFn: + """ + Builds a Monorepo with the GitHub Flow branching strategy and a merge commit merging strategy + for alpha feature releases and official releases on the default branch. + + Implementation: + - The monorepo contains two packages, each with its own internal changelog but shared template. + - The repository implements the following git graph: + + ``` + * chore(release): pkg2@1.1.0 [skip ci] (tag: pkg2-v1.1.0) + * Merge pull request #3 from 'pkg2/feat/pr-2' + |\ + | * chore(release): pkg2@1.1.0-alpha.2 [skip ci] (tag: pkg2-v1.1.0-alpha.2, branch: pkg2/feat/pr-2) + | * fix(pkg2-cli): file modified outside of pkg 2, identified by scope + | * chore(release): pkg2@1.1.0-alpha.1 [skip ci] (tag: pkg2-v1.1.0-alpha.1) + | * docs: add cli documentation + | * test: add cli tests + | * feat: no pkg scope but file in pkg 2 directory + |/ + * chore(release): pkg1@1.0.1 [skip ci] (tag: pkg1-v1.0.1) + * Merge pull request #2 from 'pkg1/fix/pr-1' + |\ + | * chore(release): pkg1@1.0.1-alpha.2 [skip ci] (tag: pkg1-v1.0.1-alpha.2, branch: pkg1/fix/pr-1) + | * fix(pkg1-cli): file modified outside of pkg 1, identified by scope + | * chore(release): pkg1@1.0.1-alpha.1 [skip ci] (tag: pkg1-v1.0.1-alpha.1) + | * fix: no pkg scope but file in pkg 1 directory + |/ + * chore(release): pkg2@1.0.0 [skip ci] (tag: pkg2-v1.0.0) # Initial release of pkg 2 + * chore(release): pkg1@1.0.0 [skip ci] (tag: pkg1-v1.0.0) # Initial release of pkg 1 + * Initial commit # Includes core functionality for both packages + ``` + """ + + def _get_repo_from_definition( + commit_type: CommitConvention, + hvcs_client_name: str = "github", + hvcs_domain: str = EXAMPLE_HVCS_DOMAIN, + tag_format_str: str | None = default_tag_format_str, + extra_configs: dict[str, TomlSerializableTypes] | None = None, + mask_initial_release: bool = True, + ignore_merge_commits: bool = True, + ) -> Sequence[RepoActions]: + stable_now_datetime = stable_now_date() + commit_timestamp_gen = ( + (stable_now_datetime + timedelta(seconds=i)).isoformat(timespec="seconds") + for i in count(step=1) + ) + pr_num_gen = (i for i in count(start=2, step=1)) + + pkg1_changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ + { + "path": monorepo_pkg1_changelog_md_file, + "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": True, + }, + { + "path": monorepo_pkg1_changelog_rst_file, + "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": True, + }, + ] + + pkg2_changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ + { + "path": monorepo_pkg2_changelog_md_file, + "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": True, + }, + { + "path": monorepo_pkg2_changelog_rst_file, + "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": True, + }, + ] + + change_to_pkg1_dir: RepoActionChangeDirectory = { + "action": RepoActionStep.CHANGE_DIRECTORY, + "details": { + "directory": monorepo_pkg1_dir, + }, + } + + change_to_pkg2_dir: RepoActionChangeDirectory = { + "action": RepoActionStep.CHANGE_DIRECTORY, + "details": { + "directory": monorepo_pkg2_dir, + }, + } + + change_to_example_project_dir: RepoActionChangeDirectory = { + "action": RepoActionStep.CHANGE_DIRECTORY, + "details": { + "directory": "/", + }, + } + + if commit_type != "conventional": + raise ValueError(f"Unsupported commit type: {commit_type}") + + pkg1_commit_parser = ConventionalCommitMonorepoParser( + options=ConventionalCommitMonorepoParserOptions( + parse_squash_commits=True, + ignore_merge_commits=ignore_merge_commits, + scope_prefix=f"{monorepo_pkg1_name}-?", + path_filters=(".",), + ) + ) + + pkg2_commit_parser = ConventionalCommitMonorepoParser( + options=ConventionalCommitMonorepoParserOptions( + parse_squash_commits=pkg1_commit_parser.options.parse_squash_commits, + ignore_merge_commits=pkg1_commit_parser.options.ignore_merge_commits, + scope_prefix=f"{monorepo_pkg2_name}-?", + path_filters=(".",), + ) + ) + + common_configs: dict[str, TomlSerializableTypes] = { + # Set the default release branch + "tool.semantic_release.branches.main": { + "match": r"^(main|master)$", + "prerelease": False, + }, + "tool.semantic_release.allow_zero_version": False, + "tool.semantic_release.changelog.exclude_commit_patterns": [r"^chore"], + "tool.semantic_release.commit_parser": f"{commit_type}-monorepo", + "tool.semantic_release.commit_parser_options.parse_squash_commits": pkg1_commit_parser.options.parse_squash_commits, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": pkg1_commit_parser.options.ignore_merge_commits, + } + + mr1_pkg1_fix_branch_name = f"{monorepo_pkg1_name}/fix/pr-1" + mr2_pkg2_feat_branch_name = f"{monorepo_pkg2_name}/feat/pr-2" + + pkg1_new_version = Version.parse( + "1.0.0", tag_format=f"{monorepo_pkg1_name}-{tag_format_str}" + ) + pkg2_new_version = Version.parse( + "1.0.0", tag_format=f"{monorepo_pkg2_name}-{tag_format_str}" + ) + + repo_construction_steps: list[RepoActions] = [ + { + "action": RepoActionStep.CREATE_MONOREPO, + "details": { + "commit_type": commit_type, + "hvcs_client_name": hvcs_client_name, + "hvcs_domain": hvcs_domain, + "post_actions": [ + { + "action": RepoActionStep.CONFIGURE_MONOREPO, + "details": { + "package_dir": monorepo_pkg1_dir, + "package_name": monorepo_pkg1_name, + "tag_format_str": pkg1_new_version.tag_format, + "mask_initial_release": mask_initial_release, + "extra_configs": { + **common_configs, + "tool.semantic_release.commit_message": ( + pkg1_cmt_msg_format := dedent( + f"""\ + chore(release): {monorepo_pkg1_name}@{{version}} [skip ci] + + Automatically generated by python-semantic-release + """ + ) + ), + # package branches "feat/" & "fix/" has prerelease suffix of "alpha" + "tool.semantic_release.branches.alpha-release": { + "match": rf"^{monorepo_pkg1_name}/(feat|fix)/.+", + "prerelease": True, + "prerelease_token": "alpha", + }, + "tool.semantic_release.commit_parser_options.scope_prefix": pkg1_commit_parser.options.scope_prefix, + "tool.semantic_release.commit_parser_options.path_filters": pkg1_commit_parser.options.path_filters, + **(extra_configs or {}), + }, + }, + }, + { + "action": RepoActionStep.CONFIGURE_MONOREPO, + "details": { + "package_dir": monorepo_pkg2_dir, + "package_name": monorepo_pkg2_name, + "tag_format_str": pkg2_new_version.tag_format, + "mask_initial_release": mask_initial_release, + "extra_configs": { + **common_configs, + "tool.semantic_release.commit_message": ( + pkg2_cmt_msg_format := dedent( + f"""\ + chore(release): {monorepo_pkg2_name}@{{version}} [skip ci] + + Automatically generated by python-semantic-release + """ + ) + ), + # package branches "feat/" & "fix/" has prerelease suffix of "alpha" + "tool.semantic_release.branches.alpha-release": { + "match": rf"^{monorepo_pkg2_name}/(feat|fix)/.+", + "prerelease": True, + "prerelease_token": "alpha", + }, + "tool.semantic_release.commit_parser_options.scope_prefix": pkg2_commit_parser.options.scope_prefix, + "tool.semantic_release.commit_parser_options.path_filters": pkg2_commit_parser.options.path_filters, + **(extra_configs or {}), + }, + }, + }, + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "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, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": bool( + commit_type == "emoji" + ), + }, + ], + commit_type, + # this parser does not matter since the commit is common + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg1_commit_parser, + ), + monorepo=True, + ), + }, + }, + ], + }, + } + ] + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg1_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg1_new_version.tag_format, + "version_py_file": monorepo_pkg1_version_py_file.relative_to( + monorepo_pkg1_dir + ), + "commit_message_format": pkg1_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg1_new_version, + "dest_files": pkg1_changelog_file_definitions, + "commit_ids": [cid_c1_initial], + }, + }, + change_to_pkg1_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg2_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg2_new_version.tag_format, + "version_py_file": monorepo_pkg2_version_py_file.relative_to( + monorepo_pkg2_dir + ), + "commit_message_format": pkg2_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg2_new_version, + "dest_files": pkg2_changelog_file_definitions, + "commit_ids": [cid_c1_initial], + }, + }, + change_to_pkg2_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + # Make a fix in package 1 and release it as an alpha release + pkg1_new_version = Version.parse( + "1.0.1-alpha.1", tag_format=pkg1_new_version.tag_format + ) + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": { + "create_branch": { + "name": mr1_pkg1_fix_branch_name, + "start_branch": DEFAULT_BRANCH_NAME, + } + }, + }, + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "pre_actions": [change_to_pkg1_dir], + "commits": convert_commit_specs_to_commit_defs( + [ + { + "cid": ( + cid_pkg1_fib1_c1_fix + := "pkg1_fix_branch_1_c1_fix" + ), + "conventional": "fix: no pkg scope but file in pkg 1 directory\n\nResolves: #123\n", + "emoji": ":bug: no pkg scope but file in pkg 1 directory\n\nResolves: #123\n", + "scipy": "MAINT: no pkg scope but file in pkg 1 directory\n\nResolves: #123\n", + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + }, + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg1_commit_parser, + ), + monorepo=True, + ), + "post_actions": [change_to_example_project_dir], + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg1_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg1_new_version.tag_format, + "version_py_file": monorepo_pkg1_version_py_file.relative_to( + monorepo_pkg1_dir + ), + "commit_message_format": pkg1_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg1_new_version, + "dest_files": pkg1_changelog_file_definitions, + "commit_ids": [cid_pkg1_fib1_c1_fix], + }, + }, + change_to_pkg1_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + # Update the fix in package 1 and release another alpha release + pkg1_new_version = Version.parse( + "1.0.1-alpha.2", tag_format=pkg1_new_version.tag_format + ) + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "commits": convert_commit_specs_to_commit_defs( + [ + { + "cid": ( + cid_pkg1_fib1_c2_fix + := "pkg1_fix_branch_1_c2_fix" + ), + "conventional": "fix(pkg1-cli): file modified outside of pkg 1, identified by scope\n\n", + "emoji": ":bug: (pkg1-cli) file modified outside of pkg 1, identified by scope", + "scipy": "MAINT:pkg1-cli: file modified outside of pkg 1, identified by scope", + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + }, + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg1_commit_parser, + ), + monorepo=True, + ), + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg1_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg1_new_version.tag_format, + "version_py_file": monorepo_pkg1_version_py_file.relative_to( + monorepo_pkg1_dir + ), + "commit_message_format": pkg1_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg1_new_version, + "dest_files": pkg1_changelog_file_definitions, + "commit_ids": [cid_pkg1_fib1_c2_fix], + }, + }, + change_to_pkg1_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + # Merge the fix branch into the default branch and formally release it + pkg1_new_version = Version.parse( + "1.0.1", tag_format=pkg1_new_version.tag_format + ) + + merge_def_type_placeholder: RepoActionGitMerge[RepoActionGitMergeDetails] = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": mr1_pkg1_fix_branch_name, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_pkg1_fib1_merge := "pkg1_fix_branch_1_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_github( + pr_number=next(pr_num_gen), + branch_name=mr1_pkg1_fix_branch_name, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg1_commit_parser, + ), + monorepo=True, + ), + }, + } + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": {"branch": DEFAULT_BRANCH_NAME}, + }, + merge_def_type_placeholder, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg1_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg1_new_version.tag_format, + "version_py_file": monorepo_pkg1_version_py_file.relative_to( + monorepo_pkg1_dir + ), + "commit_message_format": pkg1_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg1_new_version, + "dest_files": pkg1_changelog_file_definitions, + "commit_ids": [cid_pkg1_fib1_merge], + }, + }, + change_to_pkg1_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + # Make a feature branch and release it as an alpha release + pkg2_new_version = Version.parse( + "1.1.0-alpha.1", tag_format=pkg2_new_version.tag_format + ) + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": { + "create_branch": { + "name": mr2_pkg2_feat_branch_name, + "start_branch": DEFAULT_BRANCH_NAME, + }, + }, + }, + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "pre_actions": [change_to_pkg2_dir], + "commits": convert_commit_specs_to_commit_defs( + [ + { + "cid": ( + cid_pkg2_feb1_c1_feat + := "pkg2_feat_branch_1_c1_feat" + ), + "conventional": "feat: no pkg scope but file in pkg 2 directory\n", + "emoji": ":sparkles: no pkg scope but file in pkg 2 directory", + "scipy": "ENH: no pkg scope but file in pkg 2 directory", + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + }, + { + "cid": ( + cid_pkg2_feb1_c2_test + := "pkg2_feat_branch_1_c2_test" + ), + "conventional": "test: add cli tests", + "emoji": ":checkmark: add cli tests", + "scipy": "TST: add cli tests", + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + }, + { + "cid": ( + cid_pkg2_feb1_c3_docs + := "pkg2_feat_branch_1_c3_docs" + ), + "conventional": "docs: add cli documentation", + "emoji": ":memo: add cli documentation", + "scipy": "DOC: add cli documentation", + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + }, + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg2_commit_parser, + ), + monorepo=True, + ), + "post_actions": [change_to_example_project_dir], + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg2_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg2_new_version.tag_format, + "version_py_file": monorepo_pkg2_version_py_file.relative_to( + monorepo_pkg2_dir + ), + "commit_message_format": pkg2_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg2_new_version, + "dest_files": pkg2_changelog_file_definitions, + "commit_ids": [ + cid_pkg2_feb1_c1_feat, + cid_pkg2_feb1_c2_test, + cid_pkg2_feb1_c3_docs, + ], + }, + }, + change_to_pkg2_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + # Update the feat with a fix in package 2 and release another alpha release + pkg2_new_version = Version.parse( + "1.1.0-alpha.2", tag_format=pkg2_new_version.tag_format + ) + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "commits": convert_commit_specs_to_commit_defs( + [ + { + "cid": ( + cid_pkg2_feb1_c4_fix + := "pkg2_feat_branch_1_c4_fix" + ), + "conventional": "fix(pkg2-cli): file modified outside of pkg 2, identified by scope", + "emoji": ":bug: (pkg2-cli) file modified outside of pkg 2, identified by scope", + "scipy": "MAINT:pkg2-cli: file modified outside of pkg 2, identified by scope", + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + }, + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg2_commit_parser, + ), + monorepo=True, + ), + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg2_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg2_new_version.tag_format, + "version_py_file": monorepo_pkg2_version_py_file.relative_to( + monorepo_pkg2_dir + ), + "commit_message_format": pkg2_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg2_new_version, + "dest_files": pkg2_changelog_file_definitions, + "commit_ids": [cid_pkg2_feb1_c4_fix], + }, + }, + change_to_pkg2_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + # Merge the feat branch into the default branch and formally release a package 2 + pkg2_new_version = Version.parse( + "1.1.0", tag_format=pkg2_new_version.tag_format + ) + + merge_def_type_placeholder = { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": mr2_pkg2_feat_branch_name, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "cid": (cid_pkg2_feb1_merge := "pkg2_feat_branch_1_merge"), + "conventional": ( + merge_msg := format_merge_commit_msg_github( + pr_number=next(pr_num_gen), + branch_name=mr2_pkg2_feat_branch_name, + ) + ), + "emoji": merge_msg, + "scipy": merge_msg, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg2_commit_parser, + ), + monorepo=True, + ), + }, + } + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": {"branch": DEFAULT_BRANCH_NAME}, + }, + merge_def_type_placeholder, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg2_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg2_new_version.tag_format, + "version_py_file": monorepo_pkg2_version_py_file.relative_to( + monorepo_pkg2_dir + ), + "commit_message_format": pkg2_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg2_new_version, + "dest_files": pkg2_changelog_file_definitions, + "commit_ids": [cid_pkg2_feb1_merge], + }, + }, + change_to_pkg2_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + return repo_construction_steps + + return _get_repo_from_definition + + +@pytest.fixture(scope="session") +def build_monorepo_w_github_flow_w_feature_release_channel( + build_repo_from_definition: BuildRepoFromDefinitionFn, + get_repo_definition_4_github_flow_monorepo_w_feature_release_channel: GetRepoDefinitionFn, + get_cached_repo_data: GetCachedRepoDataFn, + build_repo_or_copy_cache: BuildRepoOrCopyCacheFn, + build_spec_hash_4_github_flow_monorepo_w_feature_release_channel: str, +) -> BuildSpecificRepoFn: + def _build_specific_repo_type( + repo_name: str, commit_type: CommitConvention, dest_dir: Path + ) -> Sequence[RepoActions]: + def _build_repo(cached_repo_path: Path) -> Sequence[RepoActions]: + repo_construction_steps = ( + get_repo_definition_4_github_flow_monorepo_w_feature_release_channel( + commit_type=commit_type, + ) + ) + return build_repo_from_definition(cached_repo_path, repo_construction_steps) + + build_repo_or_copy_cache( + repo_name=repo_name, + build_spec_hash=build_spec_hash_4_github_flow_monorepo_w_feature_release_channel, + build_repo_func=_build_repo, + dest_dir=dest_dir, + ) + + if not (cached_repo_data := get_cached_repo_data(proj_dirname=repo_name)): + raise ValueError("Failed to retrieve repo data from cache") + + return cached_repo_data["build_definition"] + + return _build_specific_repo_type + + +# --------------------------------------------------------------------------- # +# Test-level fixtures that will cache the built directory & set up test case # +# --------------------------------------------------------------------------- # + + +@pytest.fixture +def monorepo_w_github_flow_w_feature_release_channel_conventional_commits( + build_monorepo_w_github_flow_w_feature_release_channel: BuildSpecificRepoFn, + example_project_git_repo: ExProjectGitRepoFn, + example_project_dir: ExProjectDir, + change_to_ex_proj_dir: None, +) -> BuiltRepoResult: + repo_name = ( + monorepo_w_github_flow_w_feature_release_channel_conventional_commits.__name__ + ) + commit_type: CommitConvention = repo_name.split("_")[-2] # type: ignore[assignment] + + return { + "definition": build_monorepo_w_github_flow_w_feature_release_channel( + repo_name=repo_name, + commit_type=commit_type, + dest_dir=example_project_dir, + ), + "repo": example_project_git_repo(), + } diff --git a/tests/fixtures/monorepos/trunk_based_dev/__init__.py b/tests/fixtures/monorepos/trunk_based_dev/__init__.py new file mode 100644 index 000000000..c84f42bc8 --- /dev/null +++ b/tests/fixtures/monorepos/trunk_based_dev/__init__.py @@ -0,0 +1 @@ +from tests.fixtures.monorepos.trunk_based_dev.monorepo_w_tags import * diff --git a/tests/fixtures/monorepos/trunk_based_dev/monorepo_w_tags.py b/tests/fixtures/monorepos/trunk_based_dev/monorepo_w_tags.py new file mode 100644 index 000000000..9687e088f --- /dev/null +++ b/tests/fixtures/monorepos/trunk_based_dev/monorepo_w_tags.py @@ -0,0 +1,623 @@ +from __future__ import annotations + +from datetime import timedelta +from itertools import count +from pathlib import Path +from textwrap import dedent +from typing import TYPE_CHECKING, cast + +import pytest + +from semantic_release.cli.config import ChangelogOutputFormat +from semantic_release.commit_parser.conventional.options_monorepo import ( + ConventionalCommitMonorepoParserOptions, +) +from semantic_release.commit_parser.conventional.parser_monorepo import ( + ConventionalCommitMonorepoParser, +) +from semantic_release.version.version import Version + +import tests.conftest +import tests.const +import tests.util +from tests.const import ( + EXAMPLE_HVCS_DOMAIN, + INITIAL_COMMIT_MESSAGE, + RepoActionStep, +) + +if TYPE_CHECKING: + from typing import Sequence + + from semantic_release.commit_parser._base import CommitParser, ParserOptions + from semantic_release.commit_parser.token import ParseResult + + from tests.conftest import ( + GetCachedRepoDataFn, + GetMd5ForSetOfFilesFn, + GetStableDateNowFn, + ) + from tests.fixtures.example_project import ExProjectDir + from tests.fixtures.git_repo import ( + BuildRepoFromDefinitionFn, + BuildRepoOrCopyCacheFn, + BuildSpecificRepoFn, + BuiltRepoResult, + CommitConvention, + ConvertCommitSpecsToCommitDefsFn, + ExProjectGitRepoFn, + GetRepoDefinitionFn, + RepoActionChangeDirectory, + RepoActions, + RepoActionWriteChangelogsDestFile, + TomlSerializableTypes, + ) + + +@pytest.fixture(scope="session") +def deps_files_4_trunk_only_monorepo_w_tags( + deps_files_4_example_git_monorepo: list[Path], +) -> list[Path]: + return [ + *deps_files_4_example_git_monorepo, + # This file + Path(__file__).absolute(), + # because of imports + Path(tests.const.__file__).absolute(), + Path(tests.util.__file__).absolute(), + # because of the fixtures + Path(tests.conftest.__file__).absolute(), + ] + + +@pytest.fixture(scope="session") +def build_spec_hash_4_trunk_only_monorepo_w_tags( + get_md5_for_set_of_files: GetMd5ForSetOfFilesFn, + deps_files_4_trunk_only_monorepo_w_tags: list[Path], +) -> str: + # Generates a hash of the build spec to set when to invalidate the cache + return get_md5_for_set_of_files(deps_files_4_trunk_only_monorepo_w_tags) + + +@pytest.fixture(scope="session") +def get_repo_definition_4_trunk_only_monorepo_w_tags( + convert_commit_specs_to_commit_defs: ConvertCommitSpecsToCommitDefsFn, + monorepo_pkg1_changelog_md_file: Path, + monorepo_pkg1_changelog_rst_file: Path, + monorepo_pkg2_changelog_md_file: Path, + monorepo_pkg2_changelog_rst_file: Path, + monorepo_pkg1_name: str, + monorepo_pkg2_name: str, + monorepo_pkg1_dir: Path, + monorepo_pkg2_dir: Path, + monorepo_pkg1_version_py_file: Path, + monorepo_pkg2_version_py_file: Path, + stable_now_date: GetStableDateNowFn, + default_tag_format_str: str, +) -> GetRepoDefinitionFn: + """ + Builds a Monorepo with trunk-based development only with official releases. + + Implementation: + - The monorepo contains two packages, each with its own internal changelog but shared template. + - The repository implements the following git graph: + + ``` + * chore(release): pkg1@0.1.0 [skip ci] (tag: pkg1-v0.1.0, branch: main) + * feat(pkg1): file modified outside of pkg 1, identified by scope + * chore(release): pkg2@0.1.1 [skip ci] (tag: pkg2-v0.1.1) + * fix(pkg2-cli): file modified outside of pkg 2, identified by scope + * chore(release): pkg2@0.1.0 [skip ci] (tag: pkg2-v0.1.0) + * docs(pkg2-cli): common docs modified outside of pkg 2, identified by scope + * test: no pkg scope but add tests to package 2 directory + * feat: no pkg scope but file in pkg 2 directory + * chore(release): pkg1@0.0.1 [skip ci] (tag: pkg1-v0.0.1) + * fix: no pkg scope but file in pkg 1 directory + * Initial commit # Includes core functionality for both packages + ``` + """ + + def _get_repo_from_definition( + commit_type: CommitConvention, + hvcs_client_name: str = "github", + hvcs_domain: str = EXAMPLE_HVCS_DOMAIN, + tag_format_str: str | None = default_tag_format_str, + extra_configs: dict[str, TomlSerializableTypes] | None = None, + mask_initial_release: bool = True, + ignore_merge_commits: bool = True, + ) -> Sequence[RepoActions]: + stable_now_datetime = stable_now_date() + commit_timestamp_gen = ( + (stable_now_datetime + timedelta(seconds=i)).isoformat(timespec="seconds") + for i in count(step=1) + ) + + pkg1_changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ + { + "path": monorepo_pkg1_changelog_md_file, + "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": True, + }, + { + "path": monorepo_pkg1_changelog_rst_file, + "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": True, + }, + ] + + pkg2_changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ + { + "path": monorepo_pkg2_changelog_md_file, + "format": ChangelogOutputFormat.MARKDOWN, + "mask_initial_release": True, + }, + { + "path": monorepo_pkg2_changelog_rst_file, + "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + "mask_initial_release": True, + }, + ] + + change_to_pkg1_dir: RepoActionChangeDirectory = { + "action": RepoActionStep.CHANGE_DIRECTORY, + "details": { + "directory": monorepo_pkg1_dir, + }, + } + + change_to_pkg2_dir: RepoActionChangeDirectory = { + "action": RepoActionStep.CHANGE_DIRECTORY, + "details": { + "directory": monorepo_pkg2_dir, + }, + } + + change_to_example_project_dir: RepoActionChangeDirectory = { + "action": RepoActionStep.CHANGE_DIRECTORY, + "details": { + "directory": "/", + }, + } + + if commit_type != "conventional": + raise ValueError(f"Unsupported commit type: {commit_type}") + + pkg1_commit_parser = ConventionalCommitMonorepoParser( + options=ConventionalCommitMonorepoParserOptions( + parse_squash_commits=True, + ignore_merge_commits=ignore_merge_commits, + scope_prefix=f"{monorepo_pkg1_name}-?", + path_filters=(".",), + ) + ) + + pkg2_commit_parser = ConventionalCommitMonorepoParser( + options=ConventionalCommitMonorepoParserOptions( + parse_squash_commits=pkg1_commit_parser.options.parse_squash_commits, + ignore_merge_commits=pkg1_commit_parser.options.ignore_merge_commits, + scope_prefix=f"{monorepo_pkg2_name}-?", + path_filters=(".",), + ) + ) + + common_configs: dict[str, TomlSerializableTypes] = { + # Set the default release branch + "tool.semantic_release.branches.main": { + "match": r"^(main|master)$", + "prerelease": False, + }, + "tool.semantic_release.allow_zero_version": True, + "tool.semantic_release.changelog.exclude_commit_patterns": [r"^chore"], + "tool.semantic_release.commit_parser": f"{commit_type}-monorepo", + "tool.semantic_release.commit_parser_options.parse_squash_commits": pkg1_commit_parser.options.parse_squash_commits, + "tool.semantic_release.commit_parser_options.ignore_merge_commits": pkg1_commit_parser.options.ignore_merge_commits, + } + + pkg1_new_version = Version.parse( + "0.0.1", tag_format=f"{monorepo_pkg1_name}-{tag_format_str}" + ) + pkg2_new_version = Version.parse( + "0.1.0", tag_format=f"{monorepo_pkg2_name}-{tag_format_str}" + ) + + repo_construction_steps: list[RepoActions] = [ + { + "action": RepoActionStep.CREATE_MONOREPO, + "details": { + "commit_type": commit_type, + "hvcs_client_name": hvcs_client_name, + "hvcs_domain": hvcs_domain, + "post_actions": [ + { + "action": RepoActionStep.CONFIGURE_MONOREPO, + "details": { + "package_dir": monorepo_pkg1_dir, + "package_name": monorepo_pkg1_name, + "tag_format_str": pkg1_new_version.tag_format, + "mask_initial_release": mask_initial_release, + "extra_configs": { + **common_configs, + "tool.semantic_release.commit_message": ( + pkg1_cmt_msg_format := dedent( + f"""\ + chore(release): {monorepo_pkg1_name}@{{version}} [skip ci] + + Automatically generated by python-semantic-release + """ + ) + ), + "tool.semantic_release.commit_parser_options.scope_prefix": pkg1_commit_parser.options.scope_prefix, + "tool.semantic_release.commit_parser_options.path_filters": pkg1_commit_parser.options.path_filters, + **(extra_configs or {}), + }, + }, + }, + { + "action": RepoActionStep.CONFIGURE_MONOREPO, + "details": { + "package_dir": monorepo_pkg2_dir, + "package_name": monorepo_pkg2_name, + "tag_format_str": pkg2_new_version.tag_format, + "mask_initial_release": mask_initial_release, + "extra_configs": { + **common_configs, + "tool.semantic_release.commit_message": ( + pkg2_cmt_msg_format := dedent( + f"""\ + chore(release): {monorepo_pkg2_name}@{{version}} [skip ci] + + Automatically generated by python-semantic-release + """ + ) + ), + "tool.semantic_release.commit_parser_options.scope_prefix": pkg2_commit_parser.options.scope_prefix, + "tool.semantic_release.commit_parser_options.path_filters": pkg2_commit_parser.options.path_filters, + **(extra_configs or {}), + }, + }, + }, + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "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, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": bool( + commit_type == "emoji" + ), + }, + ], + commit_type, + # this parser does not matter since the commit is common + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg1_commit_parser, + ), + monorepo=True, + ), + }, + }, + ], + }, + } + ] + + # Make initial release for package 1 + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "pre_actions": [change_to_pkg1_dir], + "commits": convert_commit_specs_to_commit_defs( + [ + { + "cid": (cid_c2_pkg1_fix := "c2_pkg1_fix"), + "conventional": "fix: no pkg scope but file in pkg 1 directory\n\nResolves: #123\n", + "emoji": ":bug: no pkg scope but file in pkg 1 directory\n\nResolves: #123\n", + "scipy": "MAINT: no pkg scope but file in pkg 1 directory\n\nResolves: #123\n", + "datetime": next(commit_timestamp_gen), + }, + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg1_commit_parser, + ), + monorepo=True, + ), + "post_actions": [change_to_example_project_dir], + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg1_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg1_new_version.tag_format, + "version_py_file": monorepo_pkg1_version_py_file.relative_to( + monorepo_pkg1_dir + ), + "commit_message_format": pkg1_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg1_new_version, + "dest_files": pkg1_changelog_file_definitions, + "commit_ids": [cid_c1_initial, cid_c2_pkg1_fix], + }, + }, + change_to_pkg1_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "pre_actions": [change_to_pkg2_dir], + "commits": convert_commit_specs_to_commit_defs( + [ + { + "cid": (cid_c4_pkg2_feat := "c4_pkg2_feat"), + "conventional": "feat: no pkg scope but file in pkg 2 directory", + "emoji": ":sparkles: no pkg scope but file in pkg 2 directory", + "scipy": "ENH: no pkg scope but file in pkg 2 directory", + "datetime": next(commit_timestamp_gen), + }, + { + "cid": (cid_c5_pkg2_test := "c5_pkg2_test"), + "conventional": "test: no pkg scope but add tests to package 2 directory", + "emoji": ":checkmark: no pkg scope but add tests to package 2 directory", + "scipy": "TST: no pkg scope but add tests to package 2 directory", + "datetime": next(commit_timestamp_gen), + }, + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg2_commit_parser, + ), + monorepo=True, + ), + "post_actions": [change_to_example_project_dir], + }, + }, + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "commits": convert_commit_specs_to_commit_defs( + [ + { + "cid": (cid_c6_pkg2_docs := "c6_pkg2_docs"), + "conventional": "docs(pkg2-cli): common docs modified outside of pkg 2, identified by scope", + "emoji": ":book: (pkg2-cli) common docs modified outside of pkg 2, identified by scope", + "scipy": "DOC:pkg2-cli: common docs modified outside of pkg 2, identified by scope", + "datetime": next(commit_timestamp_gen), + }, + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg2_commit_parser, + ), + monorepo=True, + ), + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg2_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg2_new_version.tag_format, + "version_py_file": monorepo_pkg2_version_py_file.relative_to( + monorepo_pkg2_dir + ), + "commit_message_format": pkg2_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg2_new_version, + "dest_files": pkg2_changelog_file_definitions, + "commit_ids": [ + cid_c1_initial, + cid_c4_pkg2_feat, + cid_c5_pkg2_test, + cid_c6_pkg2_docs, + ], + }, + }, + change_to_pkg2_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + pkg2_new_version = Version.parse( + "0.1.1", tag_format=pkg2_new_version.tag_format + ) + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "commits": convert_commit_specs_to_commit_defs( + [ + { + "cid": (cid_c8_pkg2_fix := "c8_pkg2_fix"), + "conventional": "fix(pkg2-cli): file modified outside of pkg 2, identified by scope", + "emoji": ":bug: (pkg2-cli) file modified outside of pkg 2, identified by scope", + "scipy": "MAINT:pkg2-cli: file modified outside of pkg 2, identified by scope", + "datetime": next(commit_timestamp_gen), + }, + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg2_commit_parser, + ), + monorepo=True, + ), + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg2_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg2_new_version.tag_format, + "version_py_file": monorepo_pkg2_version_py_file.relative_to( + monorepo_pkg2_dir + ), + "commit_message_format": pkg2_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg2_new_version, + "dest_files": pkg2_changelog_file_definitions, + "commit_ids": [cid_c8_pkg2_fix], + }, + }, + change_to_pkg2_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + pkg1_new_version = Version.parse( + "0.1.0", tag_format=pkg1_new_version.tag_format + ) + + # Add a feature to package 1 and release + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "commits": convert_commit_specs_to_commit_defs( + [ + { + "cid": (cid_c10_pkg1_feat := "c10_pkg1_feat"), + "conventional": "feat(pkg1): file modified outside of pkg 1, identified by scope", + "emoji": ":sparkles: (pkg1) file modified outside of pkg 1, identified by scope", + "scipy": "ENH:pkg1: file modified outside of pkg 1, identified by scope", + "datetime": next(commit_timestamp_gen), + }, + ], + commit_type, + parser=cast( + "CommitParser[ParseResult, ParserOptions]", + pkg1_commit_parser, + ), + monorepo=True, + ), + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": str(pkg1_new_version), + "datetime": next(commit_timestamp_gen), + "tag_format": pkg1_new_version.tag_format, + "version_py_file": monorepo_pkg1_version_py_file.relative_to( + monorepo_pkg1_dir + ), + "commit_message_format": pkg1_cmt_msg_format, + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": pkg1_new_version, + "dest_files": pkg1_changelog_file_definitions, + "commit_ids": [cid_c10_pkg1_feat], + }, + }, + change_to_pkg1_dir, + ], + "post_actions": [change_to_example_project_dir], + }, + }, + ] + ) + + return repo_construction_steps + + return _get_repo_from_definition + + +@pytest.fixture(scope="session") +def build_trunk_only_monorepo_w_tags( + build_repo_from_definition: BuildRepoFromDefinitionFn, + get_repo_definition_4_trunk_only_monorepo_w_tags: GetRepoDefinitionFn, + get_cached_repo_data: GetCachedRepoDataFn, + build_repo_or_copy_cache: BuildRepoOrCopyCacheFn, + build_spec_hash_4_trunk_only_monorepo_w_tags: str, +) -> BuildSpecificRepoFn: + def _build_specific_repo_type( + repo_name: str, commit_type: CommitConvention, dest_dir: Path + ) -> Sequence[RepoActions]: + def _build_repo(cached_repo_path: Path) -> Sequence[RepoActions]: + repo_construction_steps = get_repo_definition_4_trunk_only_monorepo_w_tags( + commit_type=commit_type, + ) + return build_repo_from_definition(cached_repo_path, repo_construction_steps) + + build_repo_or_copy_cache( + repo_name=repo_name, + build_spec_hash=build_spec_hash_4_trunk_only_monorepo_w_tags, + build_repo_func=_build_repo, + dest_dir=dest_dir, + ) + + if not (cached_repo_data := get_cached_repo_data(proj_dirname=repo_name)): + raise ValueError("Failed to retrieve repo data from cache") + + return cached_repo_data["build_definition"] + + return _build_specific_repo_type + + +# --------------------------------------------------------------------------- # +# Test-level fixtures that will cache the built directory & set up test case # +# --------------------------------------------------------------------------- # + + +@pytest.fixture +def monorepo_w_trunk_only_releases_conventional_commits( + build_trunk_only_monorepo_w_tags: BuildSpecificRepoFn, + example_project_git_repo: ExProjectGitRepoFn, + example_project_dir: ExProjectDir, + change_to_ex_proj_dir: None, +) -> BuiltRepoResult: + repo_name = monorepo_w_trunk_only_releases_conventional_commits.__name__ + commit_type: CommitConvention = repo_name.split("_")[-2] # type: ignore[assignment] + + return { + "definition": build_trunk_only_monorepo_w_tags( + repo_name=repo_name, + commit_type=commit_type, + dest_dir=example_project_dir, + ), + "repo": example_project_git_repo(), + } 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 4c12f6a72..a36a5eb3f 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 @@ -24,7 +24,7 @@ from typing import Any, Generator, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser 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 59aa7541a..ba6bd820a 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 @@ -24,7 +24,7 @@ from typing import Any, Generator, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser 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 cc8b6b42d..bfddc1c82 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 @@ -24,7 +24,7 @@ from typing import Any, Generator, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser 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 4cb3ac1f4..1495b6dbd 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 @@ -24,7 +24,7 @@ from typing import Any, Generator, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser 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 612dac16f..4a7cb1064 100644 --- a/tests/fixtures/repos/github_flow/repo_w_default_release.py +++ b/tests/fixtures/repos/github_flow/repo_w_default_release.py @@ -24,7 +24,7 @@ from typing import Any, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser 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 b6ca93954..a224aad47 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 @@ -24,7 +24,7 @@ from typing import Any, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser 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 4e3fb87be..ab2f9effa 100644 --- a/tests/fixtures/repos/github_flow/repo_w_release_channels.py +++ b/tests/fixtures/repos/github_flow/repo_w_release_channels.py @@ -24,7 +24,7 @@ from typing import Any, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser diff --git a/tests/fixtures/repos/repo_initial_commit.py b/tests/fixtures/repos/repo_initial_commit.py index cef6eacfe..baed8fe97 100644 --- a/tests/fixtures/repos/repo_initial_commit.py +++ b/tests/fixtures/repos/repo_initial_commit.py @@ -20,7 +20,7 @@ from typing import Any, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser 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 a0b6f8b16..cd8761e58 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 @@ -24,7 +24,7 @@ from typing import Any, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser 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 557f9f38b..9b1c4c527 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 @@ -24,7 +24,7 @@ from typing import Any, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser 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 dc2362f39..201cc9c87 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 @@ -22,7 +22,7 @@ from typing import Any, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser 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 f9536183d..0f958aff3 100644 --- a/tests/fixtures/repos/trunk_based_dev/repo_w_prereleases.py +++ b/tests/fixtures/repos/trunk_based_dev/repo_w_prereleases.py @@ -23,7 +23,7 @@ from typing import Any, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser 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 6121b3699..488e6c28c 100644 --- a/tests/fixtures/repos/trunk_based_dev/repo_w_tags.py +++ b/tests/fixtures/repos/trunk_based_dev/repo_w_tags.py @@ -23,7 +23,7 @@ from typing import Any, Sequence from semantic_release.commit_parser._base import CommitParser, ParserOptions - from semantic_release.commit_parser.conventional import ( + from semantic_release.commit_parser.conventional.parser import ( ConventionalCommitParser, ) from semantic_release.commit_parser.emoji import EmojiCommitParser From 6df5e876c8682fe0753ec2f8c81eb45547e52747 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Mon, 8 Sep 2025 06:57:03 +0000 Subject: [PATCH 2/3] 10.4.0 Automatically generated by python-semantic-release --- CHANGELOG.rst | 38 +++++++++++++++++++ docs/concepts/commit_parsing.rst | 4 +- .../automatic-releases/github-actions.rst | 14 +++---- .../configuration-guides/monorepos.rst | 2 +- docs/configuration/configuration.rst | 2 +- pyproject.toml | 2 +- src/gh_action/requirements.txt | 2 +- 7 files changed, 51 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3b4e00cb5..6e1a00f45 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,44 @@ CHANGELOG ========= +.. _changelog-v10.4.0: + +v10.4.0 (2025-09-08) +==================== + +✨ Features +----------- + +* **config**: Add ``conventional-monorepo`` as valid ``commit_parser`` type (`PR#1143`_, `e18f866`_) + +* **parser**: Add new conventional-commits standard parser for monorepos, closes `#614`_ + (`PR#1143`_, `e18f866`_) + +📖 Documentation +---------------- + +* Add configuration guide for monorepo use with PSR (`PR#1143`_, `e18f866`_) + +* **commit-parsers**: Introduce conventional commit monorepo parser options & features (`PR#1143`_, + `e18f866`_) + +* **configuration**: Update ``commit_parser`` option with new ``conventional-monorepo`` value + (`PR#1143`_, `e18f866`_) + +💡 Additional Release Information +--------------------------------- + +* **config**: This release introduces a new built-in parser type that can be utilized for monorepo + projects. The type value is ``conventional-monorepo`` and when specified it will apply the + conventional commit parser to a monorepo environment. This parser has specialized options to help + handle monorepo projects as well. For more information, please refer to the [Monorepo + Docs](https://python-semantic-release.readthedocs.io/en/stable). + +.. _#614: https://github.com/python-semantic-release/python-semantic-release/issues/614 +.. _e18f866: https://github.com/python-semantic-release/python-semantic-release/commit/e18f86640a78b374a327848b9e2ba868003d1a43 +.. _PR#1143: https://github.com/python-semantic-release/python-semantic-release/pull/1143 + + .. _changelog-v10.3.2: v10.3.2 (2025-09-06) diff --git a/docs/concepts/commit_parsing.rst b/docs/concepts/commit_parsing.rst index c38954b82..296169c52 100644 --- a/docs/concepts/commit_parsing.rst +++ b/docs/concepts/commit_parsing.rst @@ -49,7 +49,7 @@ Built-in Commit Parsers The following parsers are built in to Python Semantic Release: - :ref:`ConventionalCommitParser ` -- :ref:`ConventionalCommitMonorepoParser ` *(available in ${NEW_RELEASE_TAG}+)* +- :ref:`ConventionalCommitMonorepoParser ` *(available in v10.4.0+)* - :ref:`AngularCommitParser ` *(deprecated in v9.19.0)* - :ref:`EmojiCommitParser ` - :ref:`ScipyCommitParser ` @@ -140,7 +140,7 @@ built-in Conventional Commits Monorepo Parser """""""""""""""""""""""""""""""""""" -*Introduced in ${NEW_RELEASE_TAG}* +*Introduced in v10.4.0* .. important:: In order for this parser to be effective, please review the section titled diff --git a/docs/configuration/automatic-releases/github-actions.rst b/docs/configuration/automatic-releases/github-actions.rst index c7d4b0bfb..57cd7b507 100644 --- a/docs/configuration/automatic-releases/github-actions.rst +++ b/docs/configuration/automatic-releases/github-actions.rst @@ -933,14 +933,14 @@ to the GitHub Release Assets as well. - name: Action | Semantic Version Release id: release # Adjust tag with desired version if applicable. - uses: python-semantic-release/python-semantic-release@v10.3.2 + uses: python-semantic-release/python-semantic-release@v10.4.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} git_committer_name: "github-actions" git_committer_email: "actions@users.noreply.github.com" - name: Publish | Upload to GitHub Release Assets - uses: python-semantic-release/publish-action@v10.3.2 + uses: python-semantic-release/publish-action@v10.4.0 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -1039,7 +1039,7 @@ The equivalent GitHub Action configuration would be: - name: Action | Semantic Version Release # Adjust tag with desired version if applicable. - uses: python-semantic-release/python-semantic-release@v10.3.2 + uses: python-semantic-release/python-semantic-release@v10.4.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} force: patch @@ -1098,14 +1098,14 @@ Publish Action. - name: Release submodule 1 id: release-submod-1 - uses: python-semantic-release/python-semantic-release@v10.3.2 + uses: python-semantic-release/python-semantic-release@v10.4.0 with: directory: ${{ env.SUBMODULE_1_DIR }} github_token: ${{ secrets.GITHUB_TOKEN }} - name: Release submodule 2 id: release-submod-2 - uses: python-semantic-release/python-semantic-release@v10.3.2 + uses: python-semantic-release/python-semantic-release@v10.4.0 with: directory: ${{ env.SUBMODULE_2_DIR }} github_token: ${{ secrets.GITHUB_TOKEN }} @@ -1117,7 +1117,7 @@ Publish Action. # ------------------------------------------------------------------- # - name: Publish | Upload package 1 to GitHub Release Assets - uses: python-semantic-release/publish-action@v10.3.2 + uses: python-semantic-release/publish-action@v10.4.0 if: steps.release-submod-1.outputs.released == 'true' with: directory: ${{ env.SUBMODULE_1_DIR }} @@ -1125,7 +1125,7 @@ Publish Action. tag: ${{ steps.release-submod-1.outputs.tag }} - name: Publish | Upload package 2 to GitHub Release Assets - uses: python-semantic-release/publish-action@v10.3.2 + uses: python-semantic-release/publish-action@v10.4.0 if: steps.release-submod-2.outputs.released == 'true' with: directory: ${{ env.SUBMODULE_2_DIR }} diff --git a/docs/configuration/configuration-guides/monorepos.rst b/docs/configuration/configuration-guides/monorepos.rst index 63814c5c6..1f173998c 100644 --- a/docs/configuration/configuration-guides/monorepos.rst +++ b/docs/configuration/configuration-guides/monorepos.rst @@ -6,7 +6,7 @@ Releasing Packages from a Monorepo A monorepo (mono-repository) is a software development strategy where code for multiple projects is stored in a single source control system. This approach streamlines and consolidates configuration, but introduces complexities when using automated tools like Python Semantic Release (PSR). -Previously, PSR offered limited compatibility with monorepos. As of ${NEW_RELEASE_TAG}, PSR introduces the :ref:`commit_parser-builtin-conventional-monorepo`, designed specifically for monorepo environments. To fully leverage this new parser, you must configure your monorepo as described below. +Previously, PSR offered limited compatibility with monorepos. As of v10.4.0, PSR introduces the :ref:`commit_parser-builtin-conventional-monorepo`, designed specifically for monorepo environments. To fully leverage this new parser, you must configure your monorepo as described below. .. _monorepos-config: diff --git a/docs/configuration/configuration.rst b/docs/configuration/configuration.rst index 6c5abd376..2b2278382 100644 --- a/docs/configuration/configuration.rst +++ b/docs/configuration/configuration.rst @@ -796,7 +796,7 @@ within the Git repository. Built-in parsers: * ``angular`` - :ref:`AngularCommitParser ` *(deprecated in v9.19.0)* * ``conventional`` - :ref:`ConventionalCommitParser ` *(available in v9.19.0+)* - * ``conventional-monorepo`` - :ref:`ConventionalCommitMonorepoParser ` *(available in ${NEW_RELEASE_TAG}+)* + * ``conventional-monorepo`` - :ref:`ConventionalCommitMonorepoParser ` *(available in v10.4.0+)* * ``emoji`` - :ref:`EmojiCommitParser ` * ``scipy`` - :ref:`ScipyCommitParser ` * ``tag`` - :ref:`TagCommitParser ` *(deprecated in v9.12.0)* diff --git a/pyproject.toml b/pyproject.toml index ce10f324b..22640474d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "python-semantic-release" -version = "10.3.2" +version = "10.4.0" description = "Automatic Semantic Versioning for Python projects" requires-python = "~= 3.8" license = { text = "MIT" } diff --git a/src/gh_action/requirements.txt b/src/gh_action/requirements.txt index 9d0c27391..e35c59aae 100644 --- a/src/gh_action/requirements.txt +++ b/src/gh_action/requirements.txt @@ -1 +1 @@ -python-semantic-release == 10.3.2 +python-semantic-release == 10.4.0 From 98ef722b65bd6a37492cf7ec8b0425800f719114 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Mon, 8 Sep 2025 01:20:51 -0600 Subject: [PATCH 3/3] docs(CHANGELOG): update hyperlink in v10.4.0's additional info paragraph (#1323) --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6e1a00f45..165ff864f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,11 +34,11 @@ v10.4.0 (2025-09-08) * **config**: This release introduces a new built-in parser type that can be utilized for monorepo projects. The type value is ``conventional-monorepo`` and when specified it will apply the conventional commit parser to a monorepo environment. This parser has specialized options to help - handle monorepo projects as well. For more information, please refer to the [Monorepo - Docs](https://python-semantic-release.readthedocs.io/en/stable). + handle monorepo projects as well. For more information, please refer to the `Monorepo Docs`_. .. _#614: https://github.com/python-semantic-release/python-semantic-release/issues/614 .. _e18f866: https://github.com/python-semantic-release/python-semantic-release/commit/e18f86640a78b374a327848b9e2ba868003d1a43 +.. _Monorepo Docs: /configuration/configuration-guides/monorepos.html .. _PR#1143: https://github.com/python-semantic-release/python-semantic-release/pull/1143