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..01dc69ae6 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@2896129e02bb7809d2cf0c1b8e9e795ee27acbcf # v10.2.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@b717f67f7e7e9f709357bce5a542846503ce46ec # v10.2.0 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index de75b9be0..17678d579 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -23,7 +23,7 @@ jobs: STALE_PR_CLOSURE_DAYS: 10 UNRESPONSIVE_WARNING_DAYS: 14 UNRESPONSIVE_CLOSURE_DAYS: 7 - REMINDER_WINDOW: 60 + REMINDER_WINDOW: 90 OPERATIONS_RATE_LIMIT: 330 # 1000 api/hr / 3 jobs steps: diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index f8487cf39..d7ba7aa53 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@2896129e02bb7809d2cf0c1b8e9e795ee27acbcf # v10.2.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/README.rst b/README.rst index c8fdd9f14..6f0bbc94c 100644 --- a/README.rst +++ b/README.rst @@ -18,5 +18,5 @@ The usage information and examples for this GitHub Action is available under the `GitHub Actions section`_ of `python-semantic-release.readthedocs.io`_. .. _python-semantic-release: https://pypi.org/project/python-semantic-release/ -.. _python-semantic-release.readthedocs.io: https://python-semantic-release.readthedocs.io/en/latest/ -.. _GitHub Actions section: https://python-semantic-release.readthedocs.io/en/latest/automatic-releases/github-actions.html +.. _python-semantic-release.readthedocs.io: https://python-semantic-release.readthedocs.io/en/stable/ +.. _GitHub Actions section: https://python-semantic-release.readthedocs.io/en/stable/configuration/automatic-releases/github-actions.html diff --git a/action.yml b/action.yml index 2cf7cf5ec..023aec9b7 100644 --- a/action.yml +++ b/action.yml @@ -130,6 +130,10 @@ outputs: description: | "true" if a release was made, "false" otherwise + commit_sha: + description: | + The commit SHA of the release if a release was made, otherwise an empty string + tag: description: | The Git tag corresponding to the version output diff --git a/docs/configuration/automatic-releases/github-actions.rst b/docs/configuration/automatic-releases/github-actions.rst index 5d81ea7d0..07d29bce4 100644 --- a/docs/configuration/automatic-releases/github-actions.rst +++ b/docs/configuration/automatic-releases/github-actions.rst @@ -535,6 +535,20 @@ A boolean value indicating whether a release was made. ---- +.. _gh_actions-psr-outputs-commit_sha: + +``commit_sha`` +""""""""""""""""" + +**Type:** ``string`` + +The commit SHA of the release if a release was made, otherwise an empty string. + +Example upon release: ``d4c3b2a1e0f9c8b7a6e5d4c3b2a1e0f9c8b7a6e5`` +Example when no release was made: ``""`` + +---- + .. _gh_actions-psr-outputs-version: ``version`` @@ -873,14 +887,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 +993,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 +1052,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 +1071,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 +1079,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..45d9178b3 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, ) @@ -675,6 +676,9 @@ def version( # noqa: C901 noop=opts.noop, ) + with Repo(str(runtime.repo_dir)) as git_repo: + gha_output.commit_sha = git_repo.head.commit.hexsha + if push_changes: remote_url = runtime.hvcs_client.remote_url( use_token=not runtime.ignore_token_for_push diff --git a/src/semantic_release/cli/github_actions_output.py b/src/semantic_release/cli/github_actions_output.py index 7d7782922..9c4090d31 100644 --- a/src/semantic_release/cli/github_actions_output.py +++ b/src/semantic_release/cli/github_actions_output.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from re import compile as regexp from semantic_release.globals import logger from semantic_release.version.version import Version @@ -13,9 +14,11 @@ def __init__( self, released: bool | None = None, version: Version | None = None, + commit_sha: str | None = None, ) -> None: self._released = released self._version = version + self._commit_sha = commit_sha @property def released(self) -> bool | None: @@ -45,12 +48,30 @@ def tag(self) -> str | None: def is_prerelease(self) -> bool | None: return self.version.is_prerelease if self.version is not None else None + @property + def commit_sha(self) -> str | None: + return self._commit_sha if self._commit_sha else None + + @commit_sha.setter + def commit_sha(self, value: str) -> None: + if not isinstance(value, str): + raise TypeError("output 'commit_sha' should be a string") + + if not regexp(r"^[0-9a-f]{40}$").match(value): + raise ValueError( + "output 'commit_sha' should be a valid 40-hex-character SHA" + ) + + self._commit_sha = value + def to_output_text(self) -> str: missing = set() if self.version is None: missing.add("version") if self.released is None: missing.add("released") + if self.released and self.commit_sha is None: + missing.add("commit_sha") if missing: raise ValueError( @@ -62,6 +83,7 @@ def to_output_text(self) -> str: "version": str(self.version), "tag": self.tag, "is_prerelease": str(self.is_prerelease).lower(), + "commit_sha": self.commit_sha if self.commit_sha else "", } return str.join("", [f"{key}={value!s}\n" for key, value in outputs.items()]) diff --git a/src/semantic_release/cli/util.py b/src/semantic_release/cli/util.py index 0f62d3d10..37d249c1a 100644 --- a/src/semantic_release/cli/util.py +++ b/src/semantic_release/cli/util.py @@ -9,6 +9,7 @@ from typing import Any import rich +import rich.markup import tomlkit from tomlkit.exceptions import TOMLKitError @@ -26,8 +27,7 @@ def noop_report(msg: str) -> None: Rich-prints a msg with a standard prefix to report when an action is not being taken due to a "noop" flag """ - fullmsg = "[bold cyan][:shield: NOP] " + msg - rprint(fullmsg) + rprint(f"[bold cyan][:shield: NOP] {rich.markup.escape(msg)}") def indented(msg: str, prefix: str = " " * 4) -> str: 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/e2e/cmd_version/test_version_github_actions.py b/tests/e2e/cmd_version/test_version_github_actions.py index 53917e706..11f13b90f 100644 --- a/tests/e2e/cmd_version/test_version_github_actions.py +++ b/tests/e2e/cmd_version/test_version_github_actions.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING import pytest +from pytest_lazy_fixtures.lazy_fixture import lf as lazy_fixture from tests.const import MAIN_PROG_NAME, VERSION_SUBCMD from tests.fixtures.repos import ( @@ -13,16 +14,26 @@ if TYPE_CHECKING: from tests.conftest import RunCliFn from tests.fixtures.example_project import ExProjectDir + from tests.fixtures.git_repo import BuiltRepoResult -@pytest.mark.usefixtures( - repo_w_git_flow_w_alpha_prereleases_n_conventional_commits.__name__ +@pytest.mark.parametrize( + "repo_result", + [lazy_fixture(repo_w_git_flow_w_alpha_prereleases_n_conventional_commits.__name__)], ) def test_version_writes_github_actions_output( + repo_result: BuiltRepoResult, run_cli: RunCliFn, example_project_dir: ExProjectDir, ): mock_output_file = example_project_dir / "action.out" + expected_gha_output = { + "released": str(True).lower(), + "version": "1.2.1", + "tag": "v1.2.1", + "commit_sha": "0" * 40, + "is_prerelease": str(False).lower(), + } # Act cli_cmd = [MAIN_PROG_NAME, VERSION_SUBCMD, "--patch", "--no-push"] @@ -31,6 +42,9 @@ def test_version_writes_github_actions_output( ) assert_successful_exit_code(result, cli_cmd) + # Update the expected output with the commit SHA + expected_gha_output["commit_sha"] = repo_result["repo"].head.commit.hexsha + if not mock_output_file.exists(): pytest.fail( f"Expected output file {mock_output_file} to be created, but it does not exist." @@ -42,9 +56,14 @@ def test_version_writes_github_actions_output( ) # Evaluate - assert "released" in action_outputs - assert action_outputs["released"] == "true" - assert "version" in action_outputs - assert action_outputs["version"] == "1.2.1" - assert "tag" in action_outputs - assert action_outputs["tag"] == "v1.2.1" + expected_keys = set(expected_gha_output.keys()) + actual_keys = set(action_outputs.keys()) + key_difference = expected_keys.symmetric_difference(actual_keys) + + assert not key_difference, f"Unexpected keys found: {key_difference}" + + assert expected_gha_output["released"] == action_outputs["released"] + assert expected_gha_output["version"] == action_outputs["version"] + assert expected_gha_output["tag"] == action_outputs["tag"] + assert expected_gha_output["is_prerelease"] == action_outputs["is_prerelease"] + assert expected_gha_output["commit_sha"] == action_outputs["commit_sha"] 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(), + } diff --git a/tests/unit/semantic_release/cli/test_github_actions_output.py b/tests/unit/semantic_release/cli/test_github_actions_output.py index 7d46f18ef..7b5cc2861 100644 --- a/tests/unit/semantic_release/cli/test_github_actions_output.py +++ b/tests/unit/semantic_release/cli/test_github_actions_output.py @@ -25,24 +25,27 @@ def test_version_github_actions_output_format( released: bool, version: str, is_prerelease: bool ): + commit_sha = "0" * 40 # 40 zeroes to simulate a SHA-1 hash expected_output = dedent( f"""\ released={'true' if released else 'false'} version={version} tag=v{version} is_prerelease={'true' if is_prerelease else 'false'} + commit_sha={commit_sha} """ ) output = VersionGitHubActionsOutput( released=released, version=Version.parse(version), + commit_sha=commit_sha, ) # Evaluate (expected -> actual) assert expected_output == output.to_output_text() -def test_version_github_actions_output_fails_if_missing_output(): +def test_version_github_actions_output_fails_if_missing_released_param(): output = VersionGitHubActionsOutput( version=Version.parse("1.2.3"), ) @@ -52,15 +55,28 @@ def test_version_github_actions_output_fails_if_missing_output(): output.to_output_text() +def test_version_github_actions_output_fails_if_missing_commit_sha_param(): + output = VersionGitHubActionsOutput( + released=True, + version=Version.parse("1.2.3"), + ) + + # Execute with expected failure + with pytest.raises(ValueError, match="required outputs were not set"): + output.to_output_text() + + def test_version_github_actions_output_writes_to_github_output_if_available( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ): mock_output_file = tmp_path / "action.out" version_str = "1.2.3" + commit_sha = "0" * 40 # 40 zeroes to simulate a SHA-1 hash monkeypatch.setenv("GITHUB_OUTPUT", str(mock_output_file.resolve())) output = VersionGitHubActionsOutput( version=Version.parse(version_str), released=True, + commit_sha=commit_sha, ) output.write_if_possible() @@ -73,6 +89,8 @@ def test_version_github_actions_output_writes_to_github_output_if_available( assert version_str == action_outputs["version"] assert str(True).lower() == action_outputs["released"] assert str(False).lower() == action_outputs["is_prerelease"] + assert f"v{version_str}" == action_outputs["tag"] + assert commit_sha == action_outputs["commit_sha"] def test_version_github_actions_output_no_error_if_not_in_gha( @@ -81,6 +99,7 @@ def test_version_github_actions_output_no_error_if_not_in_gha( output = VersionGitHubActionsOutput( version=Version.parse("1.2.3"), released=True, + commit_sha="0" * 40, # 40 zeroes to simulate a SHA-1 hash ) monkeypatch.delenv("GITHUB_OUTPUT", raising=False)