From 62b1e0ba9271752cb18160471fd37a9ef2baaa5e Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 7 Jun 2025 17:48:48 -0600 Subject: [PATCH 1/4] test(cmd-version): update version stamp test to verify changes are staged during execution --- tests/e2e/cmd_version/test_version_stamp.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/e2e/cmd_version/test_version_stamp.py b/tests/e2e/cmd_version/test_version_stamp.py index a12059f37..182e9406b 100644 --- a/tests/e2e/cmd_version/test_version_stamp.py +++ b/tests/e2e/cmd_version/test_version_stamp.py @@ -3,7 +3,7 @@ import json from pathlib import Path from textwrap import dedent -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest import tomlkit @@ -61,8 +61,6 @@ def test_version_only_stamp_version( post_mocker: MagicMock, example_pyproject_toml: Path, example_project_dir: ExProjectDir, - example_changelog_md: Path, - example_changelog_rst: Path, ) -> None: repo = repo_result["repo"] version_file = example_project_dir.joinpath( @@ -97,10 +95,11 @@ def test_version_only_stamp_version( head_after = repo.head.commit tags_after = {tag.name for tag in repo.tags} tags_set_difference = set.difference(tags_after, tags_before) - differing_files = [ + actual_staged_files = [ # Make sure filepath uses os specific path separators str(Path(file)) - for file in str(repo.git.diff(name_only=True)).splitlines() + # Changed files should always be staged + for file in cast("str", repo.git.diff(staged=True, name_only=True)).splitlines() ] pyproject_toml_after = tomlkit.loads( example_pyproject_toml.read_text(encoding="utf-8") @@ -125,7 +124,7 @@ def test_version_only_stamp_version( assert post_mocker.call_count == 0 # no vcs release creation occurred # Files that should receive version change - assert expected_changed_files == differing_files + assert expected_changed_files == actual_staged_files # Compare pyproject.toml assert pyproject_toml_before == pyproject_toml_after From 365525adce53d5e5442784bd377df1e9f161cf8f Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 7 Jun 2025 16:05:24 -0600 Subject: [PATCH 2/4] feat(cmd-version): always stage version stamped files & changelogs even when provided `--no-commit` Resolves: #1211 --- src/semantic_release/cli/commands/version.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/semantic_release/cli/commands/version.py b/src/semantic_release/cli/commands/version.py index 8a8cf94a1..d3aed80f3 100644 --- a/src/semantic_release/cli/commands/version.py +++ b/src/semantic_release/cli/commands/version.py @@ -142,14 +142,10 @@ def apply_version_to_source_files( if not noop: logger.debug("Updating version %s in repository files...", version) - paths = list( - map( - lambda decl, new_version=version, noop=noop: ( # type: ignore[misc] - decl.update_file_w_version(new_version=new_version, noop=noop) - ), - version_declarations, - ) - ) + paths = [ + decl.update_file_w_version(new_version=version, noop=noop) + for decl in version_declarations + ] repo_filepaths = [ str(updated_file.relative_to(repo_dir)) @@ -650,10 +646,9 @@ def version( # noqa: C901 credential_masker=runtime.masker, ) - # Preparing for committing changes + # Preparing for committing changes; we always stage files even if we're not committing them in order to support a two-stage commit + project.git_add(paths=all_paths_to_add, noop=opts.noop) if commit_changes: - project.git_add(paths=all_paths_to_add, noop=opts.noop) - # NOTE: If we haven't modified any source code then we skip trying to make a commit # and any tag that we apply will be to the HEAD commit (made outside of # running PSR From 8a2af44c7bbcfdf1519cbda5f3be0227afd72c87 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 7 Jun 2025 23:10:27 -0600 Subject: [PATCH 3/4] docs(configuration-guide): add how-to guide for `uv` integration --- docs/api/commands.rst | 2 + .../configuration-guides/index.rst | 14 + .../configuration-guides/uv_integration.rst | 327 ++++++++++++++++++ docs/configuration/index.rst | 16 +- 4 files changed, 354 insertions(+), 5 deletions(-) create mode 100644 docs/configuration/configuration-guides/index.rst create mode 100644 docs/configuration/configuration-guides/uv_integration.rst diff --git a/docs/api/commands.rst b/docs/api/commands.rst index 344c7f5f4..bdefdf884 100644 --- a/docs/api/commands.rst +++ b/docs/api/commands.rst @@ -115,6 +115,7 @@ but this command additionally ensures the updated changelog is included in the r commit that is made. .. seealso:: + - :ref:`Ultraviolet (uv) integration ` - :ref:`cmd-changelog` - :ref:`changelog-templates` - :ref:`config-tag_format` @@ -122,6 +123,7 @@ commit that is made. - :ref:`config-version_toml` - :ref:`config-version_variables` + .. _cmd-version-options: Options: diff --git a/docs/configuration/configuration-guides/index.rst b/docs/configuration/configuration-guides/index.rst new file mode 100644 index 000000000..70024dd11 --- /dev/null +++ b/docs/configuration/configuration-guides/index.rst @@ -0,0 +1,14 @@ +.. _config-guides: + +Configuration Guides +==================== + +This section provides detailed guides on how to configure PSR for various use cases and +integrations. It is recommended to complete the +:ref:`Getting Started Guide ` first before diving into these +more specific configurations. + +.. toctree:: + :maxdepth: 1 + + UV Project Setup diff --git a/docs/configuration/configuration-guides/uv_integration.rst b/docs/configuration/configuration-guides/uv_integration.rst new file mode 100644 index 000000000..cb31e79c1 --- /dev/null +++ b/docs/configuration/configuration-guides/uv_integration.rst @@ -0,0 +1,327 @@ +.. _config-guides-uv_integration: + +Ultraviolet (``uv``) Integration +================================ + +.. _uv: https://docs.astral.sh/uv/ + +`uv`_ is an extremely fast Python package and project manager that +provides a modern alternative to `pip `_ +and `venv `_. It provides a lot +of features that solve the common problems of Python package management but +it also introduces a few quirks that need to be taken into account when using +Python Semantic Release. + +.. important:: + + **Prerequisite:** Make sure you have run through the + :ref:`Getting Started Guide ` before proceeding with + this guide. + + +Updating the ``uv.lock`` +------------------------ + +One of the best features of ``uv`` is that it automatically generates a lock file +(``uv.lock``) that contains the exact versions of all the dependencies used in +your project. The lock file is generated when you run the ``uv install`` command, +and it is used to ensure that CI workflows are repeatable and development environments +are consistent. + +When creating a new release using Python Semantic Release, PSR will update the version +in the project's definition file (e.g., ``pyproject.toml``) to indicate the new version. +Unfortunately, this action will cause ``uv`` to fail on the next execution because the +lock file will be out of sync with the project's definition file. There are two ways to +resolve this issue depending on your preference: + +#. **Add a step to your build command**: Modify your + :ref:`semantic_release.build_command ` to include the command + to update the lock file and stage it for commit. This is commonly used with the + :ref:`GitHub Action ` and other CI/CD tools when you are building + the artifact at the time of release. + + .. code-block:: toml + + [tool.semantic_release] + build_command = """ + uv lock --offline + 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 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. + + 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. + The best way to ensure that the correct version of ``uv`` is installed is to define + the version of ``uv`` in an optional dependency list (e.g. ``build``). This will + also help with other automated tools like Dependabot or Renovate to keep the version + of ``uv`` up to date. + + .. code-block:: toml + + [project.optional-dependencies] + build = ["uv ~= 0.7.12"] + + [tool.semantic_release] + build_command = """ + python -m pip install -e .[build] + uv lock --offline + 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. + + .. code-block:: bash + + # 1. PSR stamps version into files (nothing else) + # don't build the changelog (especially in update mode) + 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 + + # 3. stage the lock file to ensure it is included in the PSR commit + git add uv.lock + + # 4. run PSR fully to create release + semantic-release -v version + +**Advanced Example** + +Of course, you can mix and match these 2 approaches as needed. If PSR's pipeline was using +``uv``, we would have a mixture of the 2 approaches because we run the build in a separate +job from the release. In our case, PSR would also need to carry the lock file as a workflow +artifact along the pipeline for the release job to commit it. This advanced workflow would +look like this: + +.. code-block:: text + + # File: .tool-versions + uv 0.7.12 + +.. code-block:: text + + # File: .python-version + 3.11.11 + +.. code-block:: toml + + # File: pyproject.toml + [project.optional-dependencies] + build = ["python-semantic-release ~= 10.0"] + test = ["pytest ~= 8.0"] + + [tool.semantic_release] + build_command = """ + uv lock --offline + uv build + """ + +.. code-block:: yaml + + # File: .github/workflows/release.yml + on: + push: + branches: + - main + + jobs: + + build: + runs-on: ubuntu-latest + permissions: + contents: read + env: + dist_artifacts_name: dist + dist_artifacts_dir: dist + lock_file_artifact: uv.lock + steps: + - name: Setup | Checkout Repository at workflow sha + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + + - name: Setup | Force correct release branch on workflow sha + run: git checkout -B ${{ github.ref_name }} + + - name: Setup | Install uv + uses: asdf-vm/actions/install@1902764435ca0dd2f3388eea723a4f92a4eb8302 # v4.0.2 + + - name: Setup | Install Python & Project dependencies + run: uv sync --extra build + + - name: Build | Build next version artifacts + id: version + env: + GH_TOKEN: "none" + run: uv run semantic-release -v version --no-commit --no-tag + + - name: Upload | Distribution Artifacts + if: ${{ steps.version.outputs.released == 'true' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ env.dist_artifacts_name }} + path: ${{ format('{0}/**', env.dist_artifacts_dir) }} + if-no-files-found: error + retention-days: 2 + + - name: Upload | Lock File Artifact + if: ${{ steps.version.outputs.released == 'true' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ env.lock_file_artifact }} + path: ${{ env.lock_file_artifact }} + if-no-files-found: error + retention-days: 2 + + outputs: + new-release-detected: ${{ steps.version.outputs.released }} + new-release-version: ${{ steps.version.outputs.version }} + new-release-tag: ${{ steps.version.outputs.tag }} + new-release-is-prerelease: ${{ steps.version.outputs.is_prerelease }} + distribution-artifacts: ${{ env.dist_artifacts_name }} + lock-file-artifact: ${{ env.lock_file_artifact }} + + + test-e2e: + needs: build + runs-on: ubuntu-latest + steps: + - name: Setup | Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.sha }} + fetch-depth: 1 + + - name: Setup | Download Distribution Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + if: ${{ needs.build.outputs.new-release-detected == 'true' }} + id: artifact-download + with: + name: ${{ needs.build.outputs.distribution-artifacts }} + path: ./dist + + - name: Setup | Install uv + uses: asdf-vm/actions/install@1902764435ca0dd2f3388eea723a4f92a4eb8302 # v4.0.2 + + - name: Setup | Install Python & Project dependencies + run: uv sync --extra test + + - name: Setup | Install distribution artifact + if: ${{ steps.artifact-download.outcome == 'success' }} + run: | + uv pip uninstall my-package + uv pip install dist/python_semantic_release-*.whl + + - name: Test | Run pytest + run: uv run pytest -vv tests/e2e + + + release: + runs-on: ubuntu-latest + needs: + - build + - test-e2e + + if: ${{ needs.build.outputs.new-release-detected == 'true' }} + + concurrency: + group: ${{ github.workflow }}-release-${{ github.ref_name }} + cancel-in-progress: false + + permissions: + contents: write + + steps: + - name: Setup | Checkout Repository on Release Branch + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.ref_name }} + fetch-depth: 0 + + - name: Setup | Force release branch to be at workflow sha + run: git reset --hard ${{ github.sha }} + + - name: Setup | Install uv + uses: asdf-vm/actions/install@1902764435ca0dd2f3388eea723a4f92a4eb8302 # v4.0.2 + + - name: Setup | Install Python & Project dependencies + run: uv sync --extra build + + - name: Setup | Download Build Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + id: artifact-download + with: + name: ${{ needs.build.outputs.distribution-artifacts }} + path: dist + + - name: Setup | Download Lock File Artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: ${{ needs.build.outputs.lock-file-artifact }} + + - name: Setup | Stage Lock File for Version Commit + run: git add uv.lock + + - name: Release | Create Release + id: release + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + bash .github/workflows/verify_upstream.sh + uv run semantic-release -v --strict version --skip-build + uv run semantic-release publish + + outputs: + released: ${{ steps.release.outputs.released }} + new-release-version: ${{ steps.release.outputs.version }} + new-release-tag: ${{ steps.release.outputs.tag }} + + + deploy: + name: Deploy + runs-on: ubuntu-latest + if: ${{ needs.release.outputs.released == 'true' && github.repository == 'python-semantic-release/my-package' }} + needs: + - build + - release + + environment: + name: pypi + url: https://pypi.org/project/my-package/ + + permissions: + id-token: write + + steps: + - name: Setup | Download Build Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + id: artifact-download + with: + name: ${{ needs.build.outputs.distribution-artifacts }} + path: dist + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 + with: + packages-dir: dist + print-hash: true + verbose: true diff --git a/docs/configuration/index.rst b/docs/configuration/index.rst index 3b5dade61..1044ae10d 100644 --- a/docs/configuration/index.rst +++ b/docs/configuration/index.rst @@ -3,16 +3,22 @@ Configuration ============= -Python Semantic Release is highly configurable, allowing you to tailor it to your project's needs. It supports -various runtime environments and can be integrated with different CI/CD services. +Python Semantic Release is highly configurable, allowing you to tailor it to your +project's needs. It supports various runtime environments and can be integrated with +different CI/CD services. -1. Check out the :ref:`Configuration Options ` to customize your release process. +#. Check out our set of :ref:`configuration guides ` to help walk + you through the set up of common project customizations. -2. Configure your :ref:`CI/CD services ` to use Python Semantic Release. +#. Dive in deep and explore the full set of possible :ref:`customization options `. + +#. Go Automatic and Configure your :ref:`CI/CD services ` to use Python + Semantic Release. .. toctree:: :maxdepth: 1 :hidden: - Configuration Options + Guides + Options automatic-releases/index From dc16734663ddc5d4e3d7a2076a39981f33f50b28 Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Sat, 7 Jun 2025 23:11:34 -0600 Subject: [PATCH 4/4] docs(cmd-version): improve command description & include common uses --- docs/api/commands.rst | 75 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/docs/api/commands.rst b/docs/api/commands.rst index bdefdf884..3dca77474 100644 --- a/docs/api/commands.rst +++ b/docs/api/commands.rst @@ -96,24 +96,71 @@ pipeline, while omitting this flag would allow the pipeline to continue to run. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Detect the semantically correct next version that should be applied to your -project. +project and release it. -By default: +By default (in order): - * Write this new version to the project metadata locations - specified in the configuration file - * Build the project using :ref:`config-build_command`, if specified - * Create a new commit with these locations and any other assets configured - to be included in a release - * Tag this commit according the configured format, with a tag that uniquely - identifies the version being released - * Push the new tag and commit to the remote for the repository - * Create a release (if supported) in the remote VCS for this tag + #. Write this new version to the project metadata locations + specified in the configuration file + + #. Update the changelog file with the new version and any changes + introduced since the last release, using the configured changelog template + + #. Build the project using :ref:`config-build_command`, if specified + + #. Create a new commit with these locations and any other assets configured + to be included in a release + + #. Tag this commit according the configured format, with a tag that uniquely + identifies the version being released + + #. Push the new tag and commit to the remote for the repository + + #. Create a release in the remote VCS for this tag (if supported) + +All of these steps can be toggled on or off using the command line options +described below. Some of the steps rely on others, so some options may implicitly +disable others. Changelog generation is done identically to the way it is done in :ref:`cmd-changelog`, but this command additionally ensures the updated changelog is included in the release commit that is made. + **Common Variations** + + .. code-block:: bash + + # Print the next version that will be applied + semantic-release version --print + + # Print the next version that will be applied, including the tag prefix + semantic-release version --print-tag + + # Print the last released version + semantic-release version --print-last-released + + # Print the last released version, including the tag prefix + semantic-release version --print-last-released-tag + + # Only stamp the next version in the project metadata locations + semantic-release version --no-changelog --skip-build --no-commit --no-tag + + # Stamp the version, update the changelog, and run the build command, then stop + semantic-release version --no-commit --no-tag + + # Make all local changes but do not publish them to the remote (changelog, build, commit & tag) + semantic-release version --no-push + + # Don't ever create a changelog (but do everything else) + semantic-release version --no-changelog + + # Don't create a release in the remote VCS (but do publish the commit and tag) + semantic-release version --no-vcs-release + + # Do everything + semantic-release version + + .. seealso:: - :ref:`Ultraviolet (uv) integration ` - :ref:`cmd-changelog` @@ -364,9 +411,9 @@ Whether or not to push new commits and/or tags to the remote repository. ``--vcs-release/--no-vcs-release`` ********************************** -Whether or not to create a "release" in the remote VCS service, if supported. Currently -releases in GitHub and Gitea remotes are supported. If releases aren't supported in a -remote VCS, this option will not cause a command failure, but will produce a warning. +Whether or not to create a "release" in the remote VCS service, if supported. If +releases aren't supported in a remote VCS, this option will not cause a command +failure, but will produce a warning. **Default:** ``--no-vcs-release`` if ``--no-push`` is supplied (including where this is implied by supplying only ``--no-commit``), otherwise ``--vcs-release``