diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 85652f7c5..b1115d0c4 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -20,7 +20,7 @@ updates: # Maintain dependencies for Docker (ie our GitHub Action) - package-ecosystem: "docker" - directory: "/" + directory: "src/gh_action" schedule: interval: "monthly" labels: diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 512454ab0..84d656b96 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -145,14 +145,14 @@ jobs: - name: Release | Python Semantic Release id: release - uses: python-semantic-release/python-semantic-release@1a324000f2251a9e722e77b128bf72712653813f # v10.0.2 + uses: python-semantic-release/python-semantic-release@f9e152fb36cd2e590fe8c2bf85bbff08f7fc1c52 # v10.1.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} verbosity: 1 build: false - name: Release | Add distribution artifacts to GitHub Release Assets - uses: python-semantic-release/publish-action@e5e3010f6a207cd5d6f5d3dccedbea355484ca02 # v10.0.2 + uses: python-semantic-release/publish-action@ca88900e4d435c6645d47e5f1e7f108e94c77f05 # v10.1.0 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index f8487cf39..5248ddc99 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -112,7 +112,7 @@ jobs: - name: Build | Build next version artifacts id: version - uses: python-semantic-release/python-semantic-release@1a324000f2251a9e722e77b128bf72712653813f # v10.0.2 + uses: python-semantic-release/python-semantic-release@f9e152fb36cd2e590fe8c2bf85bbff08f7fc1c52 # v10.1.0 with: github_token: "" verbosity: 1 @@ -195,7 +195,7 @@ jobs: --junit-xml=tests/reports/pytest-results.xml - name: Report | Upload Test Results - uses: mikepenz/action-junit-report@65fe03598d8d251738592a497a9e8547a5c48eaa # v5.6.0 + uses: mikepenz/action-junit-report@a83fd2b5d58d4fc702e690c1ea688d702d28d281 # v5.6.1 if: ${{ always() && steps.tests.outcome != 'skipped' }} with: report_paths: ./tests/reports/*.xml @@ -285,7 +285,7 @@ jobs: retention-days: 1 - name: Report | Upload Test Results - uses: mikepenz/action-junit-report@65fe03598d8d251738592a497a9e8547a5c48eaa # v5.6.0 + uses: mikepenz/action-junit-report@a83fd2b5d58d4fc702e690c1ea688d702d28d281 # v5.6.1 if: ${{ always() && steps.tests.outcome != 'skipped' }} with: report_paths: ./tests/reports/*.xml @@ -383,7 +383,7 @@ jobs: retention-days: 1 - name: Report | Upload Test Results - uses: mikepenz/action-junit-report@65fe03598d8d251738592a497a9e8547a5c48eaa # v5.6.0 + uses: mikepenz/action-junit-report@a83fd2b5d58d4fc702e690c1ea688d702d28d281 # v5.6.1 if: ${{ always() && steps.tests.outcome != 'skipped' }} with: report_paths: ./tests/reports/*.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f68bac6c..5ae875b90 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,7 +50,7 @@ repos: name: ruff (format) - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.16.0" + rev: "v1.16.1" hooks: - id: mypy additional_dependencies: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f7255ca13..547063c2b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,30 @@ CHANGELOG ========= +.. _changelog-v10.2.0: + +v10.2.0 (2025-06-29) +==================== + +✨ Features +----------- + +* **cmd-version**: Adds ``PACKAGE_NAME`` value into build command environment (`db9bc13`_) + +📖 Documentation +---------------- + +* **configuration**: Update build command environment definition to include ``PACKAGE_NAME`` + variable (`4aa3805`_) + +* **uv-integration**: Fix configuration guide for ``uv`` usage to ensure lock file update + (`5390145`_) + +.. _4aa3805: https://github.com/python-semantic-release/python-semantic-release/commit/4aa38059ce6b33ca23a547473e9fb8a19d3ffbe1 +.. _5390145: https://github.com/python-semantic-release/python-semantic-release/commit/5390145503b4d5dcca8f323e1ba6c5bec0bd079b +.. _db9bc13: https://github.com/python-semantic-release/python-semantic-release/commit/db9bc132c8a0398f2cce647730c69a32ca35ba51 + + .. _changelog-v10.1.0: v10.1.0 (2025-06-12) diff --git a/docs/configuration/automatic-releases/github-actions.rst b/docs/configuration/automatic-releases/github-actions.rst index 5d81ea7d0..d9a00f1f5 100644 --- a/docs/configuration/automatic-releases/github-actions.rst +++ b/docs/configuration/automatic-releases/github-actions.rst @@ -873,14 +873,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.1.0 + uses: python-semantic-release/python-semantic-release@v10.2.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.1.0 + uses: python-semantic-release/publish-action@v10.2.0 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -979,7 +979,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.1.0 + uses: python-semantic-release/python-semantic-release@v10.2.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} force: patch @@ -1038,14 +1038,14 @@ Publish Action. - name: Release submodule 1 id: release-submod-1 - uses: python-semantic-release/python-semantic-release@v10.1.0 + uses: python-semantic-release/python-semantic-release@v10.2.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.1.0 + uses: python-semantic-release/python-semantic-release@v10.2.0 with: directory: ${{ env.SUBMODULE_2_DIR }} github_token: ${{ secrets.GITHUB_TOKEN }} @@ -1057,7 +1057,7 @@ Publish Action. # ------------------------------------------------------------------- # - name: Publish | Upload package 1 to GitHub Release Assets - uses: python-semantic-release/publish-action@v10.1.0 + uses: python-semantic-release/publish-action@v10.2.0 if: steps.release-submod-1.outputs.released == 'true' with: directory: ${{ env.SUBMODULE_1_DIR }} @@ -1065,7 +1065,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.1.0 + uses: python-semantic-release/publish-action@v10.2.0 if: steps.release-submod-2.outputs.released == 'true' with: directory: ${{ env.SUBMODULE_2_DIR }} diff --git a/docs/configuration/configuration-guides/uv_integration.rst b/docs/configuration/configuration-guides/uv_integration.rst index cb31e79c1..5fc9ea180 100644 --- a/docs/configuration/configuration-guides/uv_integration.rst +++ b/docs/configuration/configuration-guides/uv_integration.rst @@ -44,18 +44,21 @@ resolve this issue depending on your preference: [tool.semantic_release] build_command = """ - uv lock --offline - git add uv.lock - uv build + uv lock --upgrade-package "$PACKAGE_NAME" + git add uv.lock + uv build """ - The ``--offline`` flag is used to ensure that the lock file is updated without - updating any dependency versions. The intent of this call is **ONLY** to update + The intent of the lock upgrade-package call is **ONLY** to update the version of your project within the lock file after PSR has updated the version in your project's definition file (e.g., ``pyproject.toml``). When you are running PSR, you have already tested the project as is and you don't want to actually update the dependencies if a new one just became available. + For ease of use, PSR provides the ``$PACKAGE_NAME`` environment variable that + contains the name of your package from the project's definition file + (``pyproject.toml:project.name``). + If you are using the :ref:`PSR GitHub Action `, you will need to add an installation command for ``uv`` to the :ref:`build_command ` because the action runs in a Docker environment does not include ``uv`` by default. @@ -71,21 +74,23 @@ resolve this issue depending on your preference: [tool.semantic_release] build_command = """ - python -m pip install -e .[build] - uv lock --offline - git add uv.lock - uv build + python -m pip install -e '.[build]' + uv lock --upgrade-package "$PACKAGE_NAME" + git add uv.lock + uv build """ #. **Stamp the code first & then separately run release**: If you prefer to not modify the - build command, then you will need to run the ``uv lock --offline`` command prior to actually - creating the release. Essentially, you will run PSR twice: (1) once to update the version - in the project's definition file, and (2) a second time to generate the release. - - The intent of the ``uv lock --offline`` command is **ONLY** to update the version of your - project within the lock file after PSR has updated the version in your project's definition - file (e.g., ``pyproject.toml``). When you are running PSR, you have already tested the project - as is and you don't want to actually update the dependencies if a new one just became available. + build command, then you will need to run the ``uv lock --upgrade-package `` + command prior to actually creating the release. Essentially, you will run PSR twice: + (1) once to update the version in the project's definition file, and (2) a second time + to generate the release. + + The intent of the ``uv lock --upgrade-package `` command is **ONLY** + to update the version of your project within the lock file after PSR has updated the + version in your project's definition file (e.g., ``pyproject.toml``). When you are + running PSR, you have already tested the project as is and you don't want to actually + update the dependencies if a new one just became available. .. code-block:: bash @@ -94,7 +99,7 @@ resolve this issue depending on your preference: semantic-release -v version --skip-build --no-commit --no-tag --no-changelog # 2. run UV lock as pyproject.toml is updated with the next version - uv lock --offline + uv lock --upgrade-package # 3. stage the lock file to ensure it is included in the PSR commit git add uv.lock @@ -129,7 +134,7 @@ look like this: [tool.semantic_release] build_command = """ - uv lock --offline + uv lock --upgrade-package "$PACKAGE_NAME" uv build """ diff --git a/docs/configuration/configuration.rst b/docs/configuration/configuration.rst index 08368b337..d9c154a93 100644 --- a/docs/configuration/configuration.rst +++ b/docs/configuration/configuration.rst @@ -210,6 +210,7 @@ GITLAB_CI Pass-through ``true`` if exists in process env, unset HOME Pass-through ``HOME`` of parent process NEW_VERSION Semantically determined next version (ex. ``1.2.3``) PATH Pass-through ``PATH`` of parent process +PACKAGE_NAME Project name as defined in ``pyproject.toml:project.name`` PSR_DOCKER_GITHUB_ACTION Pass-through ``true`` if exists in process env, unset otherwise VIRTUAL_ENV Pass-through ``VIRTUAL_ENV`` if exists in process env, unset otherwise ======================== ====================================================================== diff --git a/pyproject.toml b/pyproject.toml index 541520dc3..076826a27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "python-semantic-release" -version = "10.1.0" +version = "10.2.0" description = "Automatic Semantic Versioning for Python projects" requires-python = ">=3.8" license = { text = "MIT" } @@ -84,7 +84,7 @@ dev = [ "ruff == 0.6.1" ] mypy = [ - "mypy == 1.16.0", + "mypy == 1.16.1", "types-Deprecated ~= 1.2", "types-requests ~= 2.32.0", "types-pyyaml ~= 6.0", diff --git a/src/gh_action/requirements.txt b/src/gh_action/requirements.txt index 44b3d8407..1192d4152 100644 --- a/src/gh_action/requirements.txt +++ b/src/gh_action/requirements.txt @@ -1 +1 @@ -python-semantic-release == 10.1.0 +python-semantic-release == 10.2.0 diff --git a/src/semantic_release/cli/commands/version.py b/src/semantic_release/cli/commands/version.py index d3aed80f3..afa3f9b3d 100644 --- a/src/semantic_release/cli/commands/version.py +++ b/src/semantic_release/cli/commands/version.py @@ -632,6 +632,7 @@ def version( # noqa: C901 **runtime.build_command_env, # PSR injected environment variables "NEW_VERSION": str(new_version), + "PACKAGE_NAME": runtime.project_metadata.get("name", ""), }, noop=opts.noop, ) diff --git a/tests/const.py b/tests/const.py index 41df4533d..c7cc0a8b4 100644 --- a/tests/const.py +++ b/tests/const.py @@ -328,3 +328,4 @@ def _read_long_description(): """.lstrip() # noqa: E501 RELEASE_NOTES = "# Release Notes" +DEFAULT_MERGE_STRATEGY_OPTION = "theirs" diff --git a/tests/e2e/cmd_version/bump_version/github_flow/test_repo_1_channel_branch_update_merge.py b/tests/e2e/cmd_version/bump_version/github_flow/test_repo_1_channel_branch_update_merge.py new file mode 100644 index 000000000..1c494cfb4 --- /dev/null +++ b/tests/e2e/cmd_version/bump_version/github_flow/test_repo_1_channel_branch_update_merge.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +import tomlkit +from flatdict import FlatDict +from freezegun import freeze_time + +from tests.const import ( + DEFAULT_BRANCH_NAME, +) +from tests.fixtures.repos.github_flow import ( + repo_w_github_flow_w_default_release_n_branch_update_merge_conventional_commits, + repo_w_github_flow_w_default_release_n_branch_update_merge_emoji_commits, + repo_w_github_flow_w_default_release_n_branch_update_merge_scipy_commits, +) +from tests.util import temporary_working_directory + +if TYPE_CHECKING: + from pathlib import Path + 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, + RepoActionRelease, + RepoActions, + SplitRepoActionsByReleaseTagsFn, + ) + + +@pytest.mark.xfail( + reason="Should pass after [#1252](https://github.com/python-semantic-release/python-semantic-release/issues/1252) is fixed", +) +@pytest.mark.parametrize( + "repo_fixture_name", + [ + pytest.param(repo_fixture_name, marks=pytest.mark.comprehensive) + for repo_fixture_name in [ + repo_w_github_flow_w_default_release_n_branch_update_merge_conventional_commits.__name__, + repo_w_github_flow_w_default_release_n_branch_update_merge_emoji_commits.__name__, + repo_w_github_flow_w_default_release_n_branch_update_merge_scipy_commits.__name__, + ] + ], +) +def test_github_flow_repo_w_default_release_n_branch_update_merge( + repo_fixture_name: str, + run_psr_release: RunPSReleaseFn, + build_github_flow_repo_w_default_release_n_branch_update_merge: 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, + default_tag_format_str: str, + version_py_file: Path, + get_sanitized_md_changelog_content: GetSanitizedChangelogContentFn, + get_sanitized_rst_changelog_content: GetSanitizedChangelogContentFn, +): + # build target repo into a temporary directory + target_repo_dir = example_project_dir / repo_fixture_name + commit_type: CommitConvention = ( + repo_fixture_name.split("commits", 1)[0].split("_")[-2] # type: ignore[assignment] + ) + target_repo_definition = ( + build_github_flow_repo_w_default_release_n_branch_update_merge( + repo_name=repo_fixture_name, + commit_type=commit_type, + dest_dir=target_repo_dir, + ) + ) + target_git_repo = git_repo_for_directory(target_repo_dir) + target_repo_pyproject_toml = FlatDict( + tomlkit.loads((target_repo_dir / "pyproject.toml").read_text(encoding="utf-8")), + delimiter=".", + ) + tag_format_str: str = target_repo_pyproject_toml.get( # type: ignore[assignment] + "tool.semantic_release.tag_format", + default_tag_format_str, + ) + + # split repo actions by release actions + releasetags_2_steps: dict[str, list[RepoActions]] = ( + split_repo_actions_by_release_tags(target_repo_definition, tag_format_str) + ) + configuration_step: RepoActionConfigure = releasetags_2_steps.pop("")[0] # type: ignore[assignment] + + # Create the mirror repo directory + mirror_repo_dir = init_mirror_repo_for_rebuild( + mirror_repo_dir=(example_project_dir / "mirror"), + configuration_step=configuration_step, + ) + mirror_git_repo = git_repo_for_directory(mirror_repo_dir) + + # rebuild repo from scratch stopping before each release tag + for curr_release_tag, steps in releasetags_2_steps.items(): + # make sure mocks are clear + mocked_git_push.reset_mock() + post_mocker.reset_mock() + + # Extract expected result from target repo + head_reference_name = ( + curr_release_tag + if curr_release_tag != "Unreleased" + else DEFAULT_BRANCH_NAME + ) + target_git_repo.git.checkout(head_reference_name, detach=True) + expected_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=target_repo_dir + ) + expected_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=target_repo_dir + ) + expected_pyproject_toml_content = ( + target_repo_dir / "pyproject.toml" + ).read_text() + expected_version_file_content = (target_repo_dir / 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, + repo_construction_steps=steps[:-1], # stop before the release step + ) + release_action_step: RepoActionRelease = steps[-1] # type: ignore[assignment] + + # Act: run PSR on the repo instead of the RELEASE step + with freeze_time( + release_action_step["details"]["datetime"] + ), temporary_working_directory(mirror_repo_dir): + run_psr_release( + next_version_str=release_action_step["details"]["version"], + git_repo=mirror_git_repo, + ) + + # take measurement after running the version command + actual_release_commit_text = mirror_git_repo.head.commit.message + actual_pyproject_toml_content = (mirror_repo_dir / "pyproject.toml").read_text() + actual_version_file_content = (mirror_repo_dir / version_py_file).read_text() + actual_md_changelog_content = get_sanitized_md_changelog_content( + repo_dir=mirror_repo_dir + ) + actual_rst_changelog_content = get_sanitized_rst_changelog_content( + repo_dir=mirror_repo_dir + ) + + # Evaluate (normal release actions should have occurred as expected) + # ------------------------------------------------------------------ + # Make sure version file is updated + assert expected_pyproject_toml_content == actual_pyproject_toml_content + assert expected_version_file_content == actual_version_file_content + # Make sure changelog is updated + assert expected_md_changelog_content == actual_md_changelog_content + assert expected_rst_changelog_content == actual_rst_changelog_content + # Make sure commit is created + assert expected_release_commit_text == actual_release_commit_text + # Make sure tag is created + assert curr_release_tag in [tag.name for tag in mirror_git_repo.tags] + assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag + assert post_mocker.call_count == 1 # vcs release creation occured diff --git a/tests/e2e/cmd_version/test_version_build.py b/tests/e2e/cmd_version/test_version_build.py index 390882f9e..c9380683c 100644 --- a/tests/e2e/cmd_version/test_version_build.py +++ b/tests/e2e/cmd_version/test_version_build.py @@ -109,6 +109,7 @@ def test_version_runs_build_command( check=True, env={ "NEW_VERSION": next_release_version, # injected into environment + "PACKAGE_NAME": "", # PSR injected environment variable "CI": patched_os_environment["CI"], "BITBUCKET_CI": "true", # Converted "GITHUB_ACTIONS": patched_os_environment["GITHUB_ACTIONS"], @@ -168,6 +169,8 @@ def test_version_runs_build_command_windows( ) # Setup + package_name = "my-package" + update_pyproject_toml("project.name", package_name) built_wheel_file = get_wheel_file(next_release_version) pyproject_config = FlatDict( tomlkit.loads(example_pyproject_toml.read_text(encoding="utf-8")), @@ -205,6 +208,7 @@ def test_version_runs_build_command_windows( env={ **clean_os_environment, "NEW_VERSION": next_release_version, # injected into environment + "PACKAGE_NAME": package_name, # PSR injected environment variable "CI": patched_os_environment["CI"], "BITBUCKET_CI": "true", # Converted "GITHUB_ACTIONS": patched_os_environment["GITHUB_ACTIONS"], @@ -276,6 +280,8 @@ def test_version_runs_build_command_w_user_env( "=ignored-invalid-named-var", # TODO: validation error instead, but currently just ignore ], ) + package_name = "my-package" + update_pyproject_toml("project.name", package_name) # Mock out subprocess.run with mock.patch( @@ -309,6 +315,7 @@ def test_version_runs_build_command_w_user_env( env={ **clean_os_environment, "NEW_VERSION": next_release_version, # injected into environment + "PACKAGE_NAME": package_name, # PSR injected environment variable "CI": patched_os_environment["CI"], "BITBUCKET_CI": "true", # Converted "GITHUB_ACTIONS": patched_os_environment["GITHUB_ACTIONS"], diff --git a/tests/fixtures/git_repo.py b/tests/fixtures/git_repo.py index 106bc55e4..7ad6ac0be 100644 --- a/tests/fixtures/git_repo.py +++ b/tests/fixtures/git_repo.py @@ -33,6 +33,7 @@ from tests.const import ( COMMIT_MESSAGE, DEFAULT_BRANCH_NAME, + DEFAULT_MERGE_STRATEGY_OPTION, EXAMPLE_HVCS_DOMAIN, EXAMPLE_REPO_NAME, EXAMPLE_REPO_OWNER, @@ -233,6 +234,7 @@ def __call__( branch_name: str, commit_def: CommitDef, fast_forward: bool = True, + strategy_option: str = DEFAULT_MERGE_STRATEGY_OPTION, ) -> CommitDef: ... class CreateSquashMergeCommitFn(Protocol): @@ -241,7 +243,7 @@ def __call__( git_repo: Repo, branch_name: str, commit_def: CommitDef, - strategy_option: str = "theirs", + strategy_option: str = DEFAULT_MERGE_STRATEGY_OPTION, ) -> CommitDef: ... class CommitSpec(TypedDict): @@ -311,7 +313,7 @@ class RepoActionGitMergeDetails(DetailsBase): branch_name: str commit_def: CommitDef fast_forward: Literal[False] - # strategy_option: str + strategy_option: NotRequired[str] class RepoActionGitFFMergeDetails(DetailsBase): branch_name: str @@ -763,6 +765,7 @@ def _create_merge_commit( branch_name: str, commit_def: CommitDef, fast_forward: bool = True, + strategy_option: str = DEFAULT_MERGE_STRATEGY_OPTION, ) -> CommitDef: curr_dt = stable_now_date() commit_dt = ( @@ -784,6 +787,7 @@ def _create_merge_commit( ff=fast_forward, no_ff=bool(not fast_forward), m=commit_def["msg"], + strategy_option=strategy_option, ) # return the commit definition with the sha & message updated @@ -804,7 +808,7 @@ def _create_squash_merge_commit( git_repo: Repo, branch_name: str, commit_def: CommitDef, - strategy_option: str = "theirs", + strategy_option: str = DEFAULT_MERGE_STRATEGY_OPTION, ) -> CommitDef: curr_dt = stable_now_date() commit_dt = ( @@ -1404,6 +1408,9 @@ def _build_repo_from_definition( # noqa: C901, its required and its just test c branch_name=merge_def["branch_name"], commit_def=merge_def["commit_def"], fast_forward=merge_def["fast_forward"], + strategy_option=merge_def.get( + "strategy_option", DEFAULT_MERGE_STRATEGY_OPTION + ), ) if merge_def["commit_def"]["include_in_changelog"]: current_commits.append(merge_def["commit_def"]) diff --git a/tests/fixtures/repos/github_flow/__init__.py b/tests/fixtures/repos/github_flow/__init__.py index 47e11144d..ccf2901a6 100644 --- a/tests/fixtures/repos/github_flow/__init__.py +++ b/tests/fixtures/repos/github_flow/__init__.py @@ -1,2 +1,3 @@ from tests.fixtures.repos.github_flow.repo_w_default_release import * +from tests.fixtures.repos.github_flow.repo_w_default_release_w_branch_update_merge import * from tests.fixtures.repos.github_flow.repo_w_release_channels import * 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 new file mode 100644 index 000000000..1d6e8bf95 --- /dev/null +++ b/tests/fixtures/repos/github_flow/repo_w_default_release_w_branch_update_merge.py @@ -0,0 +1,471 @@ +from __future__ import annotations + +from datetime import timedelta +from itertools import count +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +from semantic_release.cli.config import ChangelogOutputFormat + +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 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, + FormatGitMergeCommitMsgFn, + GetRepoDefinitionFn, + RepoActions, + RepoActionWriteChangelogsDestFile, + TomlSerializableTypes, + ) + + +FEAT_BRANCH_NAME = "feat/feature" + + +@pytest.fixture(scope="session") +def deps_files_4_repo_w_default_release_n_branch_update_merge( + deps_files_4_example_git_project: list[Path], +) -> list[Path]: + return [ + *deps_files_4_example_git_project, + # 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_repo_w_default_release_n_branch_update_merge( + get_md5_for_set_of_files: GetMd5ForSetOfFilesFn, + deps_files_4_repo_w_default_release_n_branch_update_merge: 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_repo_w_default_release_n_branch_update_merge + ) + + +@pytest.fixture(scope="session") +def get_repo_definition_4_github_flow_repo_w_default_release_n_branch_update_merge( + convert_commit_specs_to_commit_defs: ConvertCommitSpecsToCommitDefsFn, + convert_commit_spec_to_commit_def: ConvertCommitSpecToCommitDefFn, + format_merge_commit_msg_git: FormatGitMergeCommitMsgFn, + changelog_md_file: Path, + changelog_rst_file: Path, + stable_now_date: GetStableDateNowFn, +) -> GetRepoDefinitionFn: + """ + This fixture provides a function that builds a repository definition for a trunk-based development + where a release in the default branch is made in parallel to a work in a feature branch, + feature branch is updated with the latest changes from the default branch and them merged back + into the default branch with a release. + + It is the minimal reproducible example of the issue + [#1252](https://github.com/python-semantic-release/python-semantic-release/issues/1252). + """ + + def _get_repo_from_definition( + commit_type: CommitConvention, + 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, + 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) + ) + + changelog_file_definitions: Sequence[RepoActionWriteChangelogsDestFile] = [ + { + "path": changelog_md_file, + "format": ChangelogOutputFormat.MARKDOWN, + }, + { + "path": changelog_rst_file, + "format": ChangelogOutputFormat.RESTRUCTURED_TEXT, + }, + ] + + repo_construction_steps: list[RepoActions] = [] + + repo_construction_steps.append( + { + "action": RepoActionStep.CONFIGURE, + "details": { + "commit_type": commit_type, + "hvcs_client_name": hvcs_client_name, + "hvcs_domain": hvcs_domain, + "tag_format_str": tag_format_str, + "mask_initial_release": mask_initial_release, + "extra_configs": { + # Set the default release branch + "tool.semantic_release.branches.main": { + "match": r"^(main|master)$", + "prerelease": False, + }, + "tool.semantic_release.allow_zero_version": True, + **(extra_configs or {}), + }, + }, + } + ) + + # Make initial release + new_version = "0.1.0" + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "commits": convert_commit_specs_to_commit_defs( + [ + { + "conventional": INITIAL_COMMIT_MESSAGE, + "emoji": INITIAL_COMMIT_MESSAGE, + "scipy": INITIAL_COMMIT_MESSAGE, + "datetime": next(commit_timestamp_gen), + "include_in_changelog": bool( + commit_type == "emoji" + ), + }, + { + "conventional": "feat: add new feature", + "emoji": ":sparkles: add new feature", + "scipy": "ENH: add new feature", + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + }, + ], + commit_type, + ), + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": new_version, + "datetime": next(commit_timestamp_gen), + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": new_version, + "dest_files": changelog_file_definitions, + }, + }, + ], + }, + }, + ] + ) + + # Create a feature branch (without commits yet, just to pin a commit) + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": { + "create_branch": { + "name": FEAT_BRANCH_NAME, + "start_branch": DEFAULT_BRANCH_NAME, + } + }, + }, + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": {"branch": DEFAULT_BRANCH_NAME}, + }, + ] + ) + + # Make another release in default branch + new_version = "0.2.0" + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "commits": convert_commit_specs_to_commit_defs( + [ + { + "conventional": "feat: add another feature", + "emoji": ":sparkles: add another feature", + "scipy": "ENH: add another feature", + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + }, + ], + commit_type, + ), + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": new_version, + "datetime": next(commit_timestamp_gen), + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": new_version, + "dest_files": changelog_file_definitions, + }, + }, + ], + }, + }, + ] + ) + + # Add commit to the feature branch + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": {"branch": FEAT_BRANCH_NAME}, + }, + { + "action": RepoActionStep.MAKE_COMMITS, + "details": { + "commits": convert_commit_specs_to_commit_defs( + [ + { + "conventional": "feat: add new feature in the feature branch", + "emoji": ":sparkles: add new feature in the feature branch", + "scipy": "ENH: add new feature in the feature branch", + "datetime": next(commit_timestamp_gen), + "include_in_changelog": True, + }, + ], + commit_type, + ), + }, + }, + ] + ) + + # Merge default branch into the feature branch to keep it up to date + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": DEFAULT_BRANCH_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "conventional": format_merge_commit_msg_git( + branch_name=DEFAULT_BRANCH_NAME, + tgt_branch_name=FEAT_BRANCH_NAME, + ), + "emoji": format_merge_commit_msg_git( + branch_name=DEFAULT_BRANCH_NAME, + tgt_branch_name=FEAT_BRANCH_NAME, + ), + "scipy": format_merge_commit_msg_git( + branch_name=DEFAULT_BRANCH_NAME, + tgt_branch_name=FEAT_BRANCH_NAME, + ), + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + ), + }, + }, + { + "action": RepoActionStep.GIT_CHECKOUT, + "details": {"branch": DEFAULT_BRANCH_NAME}, + }, + ] + ) + + # Merge the feature branch into the default branch and make a release + new_version = "0.3.0" + + repo_construction_steps.extend( + [ + { + "action": RepoActionStep.GIT_MERGE, + "details": { + "branch_name": FEAT_BRANCH_NAME, + "fast_forward": False, + "commit_def": convert_commit_spec_to_commit_def( + { + "conventional": format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_NAME, + tgt_branch_name=DEFAULT_BRANCH_NAME, + ), + "emoji": format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_NAME, + tgt_branch_name=DEFAULT_BRANCH_NAME, + ), + "scipy": format_merge_commit_msg_git( + branch_name=FEAT_BRANCH_NAME, + tgt_branch_name=DEFAULT_BRANCH_NAME, + ), + "datetime": next(commit_timestamp_gen), + "include_in_changelog": not ignore_merge_commits, + }, + commit_type, + ), + }, + }, + { + "action": RepoActionStep.RELEASE, + "details": { + "version": new_version, + "datetime": next(commit_timestamp_gen), + "pre_actions": [ + { + "action": RepoActionStep.WRITE_CHANGELOGS, + "details": { + "new_version": new_version, + "dest_files": changelog_file_definitions, + }, + }, + ], + }, + }, + ] + ) + + return repo_construction_steps + + return _get_repo_from_definition + + +@pytest.fixture(scope="session") +def build_github_flow_repo_w_default_release_n_branch_update_merge( + build_repo_from_definition: BuildRepoFromDefinitionFn, + get_repo_definition_4_github_flow_repo_w_default_release_n_branch_update_merge: GetRepoDefinitionFn, + get_cached_repo_data: GetCachedRepoDataFn, + build_repo_or_copy_cache: BuildRepoOrCopyCacheFn, + build_spec_hash_4_repo_w_default_release_n_branch_update_merge: 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_repo_w_default_release_n_branch_update_merge( + 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_repo_w_default_release_n_branch_update_merge, + 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 repo_w_github_flow_w_default_release_n_branch_update_merge_conventional_commits( + build_github_flow_repo_w_default_release_n_branch_update_merge: BuildSpecificRepoFn, + example_project_git_repo: ExProjectGitRepoFn, + example_project_dir: ExProjectDir, + change_to_ex_proj_dir: None, +) -> BuiltRepoResult: + repo_name = repo_w_github_flow_w_default_release_n_branch_update_merge_conventional_commits.__name__ + commit_type: CommitConvention = repo_name.split("_")[-2] # type: ignore[assignment] + + return { + "definition": build_github_flow_repo_w_default_release_n_branch_update_merge( + repo_name=repo_name, + commit_type=commit_type, + dest_dir=example_project_dir, + ), + "repo": example_project_git_repo(), + } + + +@pytest.fixture +def repo_w_github_flow_w_default_release_n_branch_update_merge_emoji_commits( + build_github_flow_repo_w_default_release_n_branch_update_merge: BuildSpecificRepoFn, + example_project_git_repo: ExProjectGitRepoFn, + example_project_dir: ExProjectDir, + change_to_ex_proj_dir: None, +) -> BuiltRepoResult: + repo_name = repo_w_github_flow_w_default_release_n_branch_update_merge_emoji_commits.__name__ + commit_type: CommitConvention = repo_name.split("_")[-2] # type: ignore[assignment] + + return { + "definition": build_github_flow_repo_w_default_release_n_branch_update_merge( + repo_name=repo_name, + commit_type=commit_type, + dest_dir=example_project_dir, + ), + "repo": example_project_git_repo(), + } + + +@pytest.fixture +def repo_w_github_flow_w_default_release_n_branch_update_merge_scipy_commits( + build_github_flow_repo_w_default_release_n_branch_update_merge: BuildSpecificRepoFn, + example_project_git_repo: ExProjectGitRepoFn, + example_project_dir: ExProjectDir, + change_to_ex_proj_dir: None, +) -> BuiltRepoResult: + repo_name = repo_w_github_flow_w_default_release_n_branch_update_merge_scipy_commits.__name__ + commit_type: CommitConvention = repo_name.split("_")[-2] # type: ignore[assignment] + + return { + "definition": build_github_flow_repo_w_default_release_n_branch_update_merge( + repo_name=repo_name, + commit_type=commit_type, + dest_dir=example_project_dir, + ), + "repo": example_project_git_repo(), + } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy