diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d8ae2fb03..e412f4b3f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -4,10 +4,68 @@ name: Checks on: pull_request: +permissions: + contents: read + jobs: + + eval-changes: + name: Evaluate changes + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Evaluate | Check specific file types for changes + id: changed-files + uses: tj-actions/changed-files@v45.0.2 + with: + files_yaml: | + build: + - MANIFEST.in + - Dockerfile + - .dockerignore + - scripts/** + ci: + - .github/workflows/** + docs: + - docs/** + - README.rst + - AUTHORS.rst + - CONTRIBUTING.rst + - CHANGELOG.rst + src: + - semantic_release/** + - pyproject.toml + tests: + - tests/** + + - name: Evaluate | Detect if any of the combinations of file sets have changed + id: all-changes + run: | + printf '%s\n' "any_changed=false" >> $GITHUB_OUTPUT + if [ "${{ steps.changed-files.outputs.build_any_changed }}" == "true" ] || \ + [ "${{ steps.changed-files.outputs.ci_any_changed }}" == "true" ] || \ + [ "${{ steps.changed-files.outputs.docs_any_changed }}" == "true" ] || \ + [ "${{ steps.changed-files.outputs.src_any_changed }}" == "true" ] || \ + [ "${{ steps.changed-files.outputs.tests_any_changed }}" == "true" ]; then + printf '%s\n' "any_changed=true" >> $GITHUB_OUTPUT + fi + + outputs: + any-file-changes: ${{ steps.all-changes.outputs.any_changed }} + build-changes: ${{ steps.changed-files.outputs.build_any_changed }} + ci-changes: ${{ steps.changed-files.outputs.ci_any_changed }} + doc-changes: ${{ steps.changed-files.outputs.docs_any_changed }} + src-changes: ${{ steps.changed-files.outputs.src_any_changed }} + test-changes: ${{ steps.changed-files.outputs.tests_any_changed }} + + test-linux: name: Python ${{ matrix.python-version }} on ${{ matrix.os }} tests runs-on: ${{ matrix.os }} + needs: eval-changes + if: ${{ needs.eval-changes.outputs.src-changes == 'true' || needs.eval-changes.outputs.test-changes == 'true' || needs.eval-changes.outputs.ci-changes == 'true' }} strategy: matrix: python-version: @@ -66,6 +124,8 @@ jobs: test-windows: name: Python ${{ matrix.python-version }} on ${{ matrix.os }} tests runs-on: ${{ matrix.os }} + needs: eval-changes + if: ${{ needs.eval-changes.outputs.src-changes == 'true' || needs.eval-changes.outputs.test-changes == 'true' || needs.eval-changes.outputs.ci-changes == 'true' }} strategy: # Since the current test suite takes 10-15 minutes to complete on windows, we are # only going to run it on the oldest version of python we support. The older version @@ -122,7 +182,10 @@ jobs: report_paths: ./tests/reports/*.xml annotate_only: true + lint: + needs: eval-changes + if: ${{ needs.eval-changes.outputs.any-file-changes == 'true' }} runs-on: ubuntu-latest steps: @@ -147,6 +210,7 @@ jobs: - name: mypy run: python -m mypy --ignore-missing-imports semantic_release + commitlint: runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4acf01e2e..842903553 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: # Linters and validation - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.0 + rev: v0.6.1 hooks: - id: ruff name: ruff (lint) @@ -50,7 +50,7 @@ repos: name: ruff (format) - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.10.0" + rev: "v1.11.2" hooks: - id: mypy additional_dependencies: diff --git a/CHANGELOG.md b/CHANGELOG.md index 83681b66b..9fc9c6892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # CHANGELOG +## v9.8.9 (2024-09-27) + +### Documentation + +* docs: update docstrings to resolve sphinx failures (#1030) + +set `ignore-module-all` for `autodoc_default_options` to resolve some +Sphinx errors about duplicate / ambiguous references +https://github.com/sphinx-doc/sphinx/issues/4961#issuecomment-1543858623 + +Standardize some non-standard (Google-ish) docstrings to Sphinx +format, to avoid ruff and Sphinx arguing about underline length. + +Fix indents and other minor whitespace / formatting changes. + +Fixes #1029 ([`d84efc7`](https://github.com/python-semantic-release/python-semantic-release/commit/d84efc7719a8679e6979d513d1c8c60904af7384)) + +### Fix + +* fix(version-cmd): improve `version_variables` flexibility w/ quotes (ie. json, yaml, etc) (#1028) + +* fix(version-cmd): increase `version_variable` flexibility with quotations (ie. json, yaml, etc) + + Previously json would not work due to the key being wrapped in quotes, yaml also has issues + when it does not usually use quotes. The regex we created originally only wrapped the version + to be replaced in quotes but now both the key and version can optionally be wrapped in + different kind of quotations. + + Resolves: #601, #706, #962, #1026 + +* docs(configuration): add clarity to `version_variables` usage & limitations + + Ref: #941 + +* fix(version-cmd): ensure `version_variables` do not match partial variable names + +* build(deps-test): add `PyYAML` as a test dependency + +* test(fixtures): refactor location of fixture for global use of cli runner + +* test(stamp-version): add test cases to stamp json, python, & yaml files ([`156915c`](https://github.com/python-semantic-release/python-semantic-release/commit/156915c7d759098f65cf9de7c4e980b40b38d5f1)) + ## v9.8.8 (2024-09-01) ### Documentation diff --git a/README.rst b/README.rst index 70167f553..c8fdd9f14 100644 --- a/README.rst +++ b/README.rst @@ -1,50 +1,22 @@ Python Semantic Release *********************** -|Ruff| |Test Status| |PyPI Version| |conda-forge version| |Read the Docs Status| |Pre-Commit Enabled| +*Automating Releases via SemVer and Commit Message Conventions* -Automatic Semantic Versioning for Python projects. This is a Python -implementation of `semantic-release`_ for JS by Stephan Bönnemann. If -you find this topic interesting you should check out his `talk from -JSConf Budapest`_. +---- -The general idea is to be able to detect what the next version of the -project should be based on the commits. This tool will use that to -automate the whole release, upload to an artifact repository and post changelogs to -GitHub. You can run the tool on a CI service, or just run it locally. +The official documentation for Python Semantic Release can be found at +`python-semantic-release.readthedocs.io`_. -Installation -============ +GitHub Action +============= -:: +When using the Python Semantic Release GitHub Action, it executes the command +``semantic-release version`` using `python-semantic-release`_. - python3 -m pip install python-semantic-release - semantic-release --help +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 is also available from `conda-forge`_ or as a `GitHub Action`_. -Read more about the setup and configuration in our `getting started guide`_. - -.. _semantic-release: https://github.com/semantic-release/semantic-release -.. _talk from JSConf Budapest: https://www.youtube.com/watch?v=tc2UgG5L7WM -.. _getting started guide: https://python-semantic-release.readthedocs.io/en/latest/#getting-started -.. _GitHub Action: https://python-semantic-release.readthedocs.io/en/latest/automatic-releases/github-actions.html -.. _conda-forge: https://anaconda.org/conda-forge/python-semantic-release - -.. |Test Status| image:: https://img.shields.io/github/actions/workflow/status/python-semantic-release/python-semantic-release/main.yml?branch=master&label=Test%20Status&logo=github - :target: https://github.com/python-semantic-release/python-semantic-release/actions/workflows/main.yml - :alt: test-status -.. |PyPI Version| image:: https://img.shields.io/pypi/v/python-semantic-release?label=PyPI&logo=pypi - :target: https://pypi.org/project/python-semantic-release/ - :alt: pypi -.. |conda-forge Version| image:: https://img.shields.io/conda/vn/conda-forge/python-semantic-release?logo=anaconda - :target: https://anaconda.org/conda-forge/python-semantic-release - :alt: conda-forge -.. |Read the Docs Status| image:: https://img.shields.io/readthedocs/python-semantic-release?label=Read%20the%20Docs&logo=Read%20the%20Docs - :target: https://python-semantic-release.readthedocs.io/en/latest/ - :alt: docs -.. |Pre-Commit Enabled| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit - :target: https://github.com/pre-commit/pre-commit - :alt: pre-commit -.. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json - :target: https://github.com/astral-sh/ruff - :alt: Ruff +.. _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 diff --git a/action.yml b/action.yml index 13ea3593b..dd3c1547f 100644 --- a/action.yml +++ b/action.yml @@ -1,7 +1,10 @@ --- name: Python Semantic Release -description: Automatic Semantic Versioning for Python projects +description: Automated Releases via SemVer and Commit Message Conventions + +branding: + color: orange inputs: root_options: @@ -103,14 +106,14 @@ outputs: description: | "true" if a release was made, "false" otherwise - version: - description: | - The newly released version if one was made, otherwise the current version - tag: description: | The Git tag corresponding to the version output + version: + description: | + The newly released version if one was made, otherwise the current version + runs: using: docker image: Dockerfile diff --git a/docs/automatic-releases/github-actions.rst b/docs/automatic-releases/github-actions.rst index a02e7ab1a..ca6b8661f 100644 --- a/docs/automatic-releases/github-actions.rst +++ b/docs/automatic-releases/github-actions.rst @@ -53,7 +53,7 @@ Example Workflow - name: Python Semantic Release # Adjust tag with desired version if applicable. Version shorthand # is NOT available, e.g. vX or vX.X will not work. - uses: python-semantic-release/python-semantic-release@v9.8.8 + uses: python-semantic-release/python-semantic-release@v9.8.9 with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -94,13 +94,13 @@ multiple projects. .. code:: yaml - name: Release Project 1 - uses: python-semantic-release/python-semantic-release@v9.8.8 + uses: python-semantic-release/python-semantic-release@v9.8.9 with: directory: ./project1 github_token: ${{ secrets.GITHUB_TOKEN }} - name: Release Project 2 - uses: python-semantic-release/python-semantic-release@v9.8.8 + uses: python-semantic-release/python-semantic-release@v9.8.9 with: directory: ./project2 github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/conf.py b/docs/conf.py index 9c026c4b8..da9f2cda5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,6 +16,8 @@ "sphinxcontrib.apidoc", ] +autodoc_default_options = {"ignore-module-all": True} + templates_path = ["_templates"] source_suffix = ".rst" master_doc = "index" diff --git a/docs/configuration.rst b/docs/configuration.rst index e2bfe3bbc..ee63a99b0 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -1102,4 +1102,33 @@ specified in ``file:variable`` format. For example: "docs/conf.py:version", ] +Each version variable will be transformed into a Regular Expression that will be used +to substitute the version number in the file. The replacement algorithm is **ONLY** a +pattern match and replace. It will **NOT** evaluate the code nor will PSR understand +any internal object structures (ie. ``file:object.version`` will not work). + +.. important:: + The Regular Expression expects a version value to exist in the file to be replaced. + It cannot be an empty string or a non-semver compliant string. If this is the very + first time you are using PSR, we recommend you set the version to ``0.0.0``. This + may become more flexible in the future with resolution of issue `#941`_. + +.. _#941: https://github.com/python-semantic-release/python-semantic-release/issues/941 + +Given the pattern matching nature of this feature, the Regular Expression is able to +support most file formats as a variable declaration in most languages is very similar. +We specifically support Python, YAML, and JSON as these have been the most common +requests. This configuration option will also work regardless of file extension +because its only a pattern match. + +.. note:: + This will also work for TOML but we recommend using :ref:`config-version_toml` for + TOML files as it actually will interpret the TOML file and replace the version + number before writing the file back to disk. + +.. warning:: + If the file (ex. JSON) you are replacing has two of the same variable name in it, + this pattern match will not be able to differentiate between the two and will replace + both. This is a limitation of the pattern matching and not a bug. + **Default:** ``[]`` diff --git a/docs/github-action.rst b/docs/github-action.rst index 35faf9deb..24a539f77 100644 --- a/docs/github-action.rst +++ b/docs/github-action.rst @@ -131,7 +131,7 @@ provide the following inputs: - name: Python Semantic Release # Adjust tag with desired version if applicable. Version shorthand # is NOT available, e.g. vX or vX.X will not work. - uses: python-semantic-release/python-semantic-release@v9.8.8 + uses: python-semantic-release/python-semantic-release@v9.8.9 with: # ... other options force: "patch" diff --git a/docs/index.rst b/docs/index.rst index 210d820a9..b51b47128 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,54 @@ -.. include:: ../README.rst +Python Semantic Release +*********************** + +|Ruff| |Test Status| |PyPI Version| |conda-forge version| |Read the Docs Status| |Pre-Commit Enabled| + +Automatic Semantic Versioning for Python projects. This is a Python +implementation of `semantic-release`_ for JS by Stephan Bönnemann. If +you find this topic interesting you should check out his `talk from +JSConf Budapest`_. + +The general idea is to be able to detect what the next version of the +project should be based on the commits. This tool will use that to +automate the whole release, upload to an artifact repository and post changelogs to +GitHub. You can run the tool on a CI service, or just run it locally. + +Installation +============ + +:: + + python3 -m pip install python-semantic-release + semantic-release --help + +Python Semantic Release is also available from `conda-forge`_ or as a `GitHub Action`_. +Read more about the setup and configuration in our `getting started guide`_. + +.. _semantic-release: https://github.com/semantic-release/semantic-release +.. _talk from JSConf Budapest: https://www.youtube.com/watch?v=tc2UgG5L7WM +.. _getting started guide: https://python-semantic-release.readthedocs.io/en/latest/#getting-started +.. _GitHub Action: https://python-semantic-release.readthedocs.io/en/latest/automatic-releases/github-actions.html +.. _conda-forge: https://anaconda.org/conda-forge/python-semantic-release + +.. |Test Status| image:: https://img.shields.io/github/actions/workflow/status/python-semantic-release/python-semantic-release/main.yml?branch=master&label=Test%20Status&logo=github + :target: https://github.com/python-semantic-release/python-semantic-release/actions/workflows/main.yml + :alt: test-status +.. |PyPI Version| image:: https://img.shields.io/pypi/v/python-semantic-release?label=PyPI&logo=pypi + :target: https://pypi.org/project/python-semantic-release/ + :alt: pypi +.. |conda-forge Version| image:: https://img.shields.io/conda/vn/conda-forge/python-semantic-release?logo=anaconda + :target: https://anaconda.org/conda-forge/python-semantic-release + :alt: conda-forge +.. |Read the Docs Status| image:: https://img.shields.io/readthedocs/python-semantic-release?label=Read%20the%20Docs&logo=Read%20the%20Docs + :target: https://python-semantic-release.readthedocs.io/en/latest/ + :alt: docs +.. |Pre-Commit Enabled| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit + :target: https://github.com/pre-commit/pre-commit + :alt: pre-commit +.. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + Documentation Contents ====================== diff --git a/pyproject.toml b/pyproject.toml index f7785e7e5..d70c1ecc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "python-semantic-release" -version = "9.8.8" +version = "9.8.9" description = "Automatic Semantic Versioning for Python projects" requires-python = ">=3.8" license = { text = "MIT" } @@ -59,6 +59,7 @@ docs = [ ] test = [ "coverage[toml] ~= 7.0", + "pyyaml ~= 6.0", "pytest ~= 8.3", "pytest-env ~= 1.0", "pytest-xdist ~= 3.0", @@ -73,10 +74,10 @@ test = [ dev = [ "pre-commit ~= 3.5", "tox ~= 4.11", - "ruff == 0.5.0" + "ruff == 0.6.1" ] mypy = [ - "mypy == 1.10.1", + "mypy == 1.11.2", "types-requests ~= 2.32.0", ] @@ -86,8 +87,8 @@ env = [ ] addopts = [ # TO DEBUG in single process, swap auto to 0 - "-nauto", - # "-n0", + # "-nauto", + "-n0", "-ra", "--diff-symbols", "--cache-clear", diff --git a/semantic_release/__init__.py b/semantic_release/__init__.py index a1b08b69b..30801902c 100644 --- a/semantic_release/__init__.py +++ b/semantic_release/__init__.py @@ -24,7 +24,7 @@ tags_and_versions, ) -__version__ = "9.8.8" +__version__ = "9.8.9" __all__ = [ "CommitParser", diff --git a/semantic_release/cli/commands/version.py b/semantic_release/cli/commands/version.py index 503d7bf1e..5461cdd2d 100644 --- a/semantic_release/cli/commands/version.py +++ b/semantic_release/cli/commands/version.py @@ -219,19 +219,12 @@ def build_distributions( """ Run the build command to build the distributions. - Arguments: - --------- - build_command: str | None - The build command to run - build_command_env: Mapping[str, str] | None - The environment variables to use when running the build command - noop: bool - Whether or not to run the build command - - Raises: - ------ - BuildDistributionsError: if the build command fails + :param build_command: The build command to run. + :param build_command_env: The environment variables to use when running the + build command. + :param noop: Whether or not to run the build command. + :raises: BuildDistributionsError: if the build command fails """ if not build_command: rprint("[green]No build command specified, skipping") diff --git a/semantic_release/cli/config.py b/semantic_release/cli/config.py index ba12f4ce4..5c9f31ef7 100644 --- a/semantic_release/cli/config.py +++ b/semantic_release/cli/config.py @@ -540,7 +540,18 @@ def from_raw_config( # noqa: C901 try: path, variable = decl.split(":", maxsplit=1) # VersionDeclarationABC handles path existence check - search_text = rf"(?x){variable}\s*(:=|[:=])\s*(?P['\"])(?P{SEMVER_REGEX.pattern})(?P=quote)" # noqa: E501 + search_text = str.join( + "", + [ + # Supports optional matching quotations around variable name + # Negative lookbehind to ensure we don't match part of a variable name + f"""(?x)(?P['"])?(?['"])?(?P{SEMVER_REGEX.pattern})(?P=quote2)?""", + ], + ) pd = PatternVersionDeclaration(path, search_text) except ValueError as exc: log.exception("Invalid variable declaration %r", decl) diff --git a/semantic_release/hvcs/gitea.py b/semantic_release/hvcs/gitea.py index 12f52e850..3ae2e6ab4 100644 --- a/semantic_release/hvcs/gitea.py +++ b/semantic_release/hvcs/gitea.py @@ -95,11 +95,9 @@ def create_release( Ref: https://gitea.com/api/swagger#/repository/repoCreateRelease :param tag: Tag to create release for - :param release_notes: The release notes for this version - :param prerelease: Whether or not this release should be specified as a - prerelease + prerelease :return: Whether the request succeeded """ @@ -181,6 +179,7 @@ def get_release_id_by_tag(self, tag: str) -> int | None: Get a release by its tag name https://gitea.com/api/swagger#/repository/repoGetReleaseByTag :param tag: Tag to get release for + :return: ID of found release """ tag_endpoint = self.create_api_url( @@ -206,6 +205,7 @@ def edit_release_notes(self, release_id: int, release_notes: str) -> int: https://gitea.com/api/swagger#/repository/repoEditRelease :param id: ID of release to update :param release_notes: The release notes for this version + :return: The ID of the release that was edited """ log.info("Updating release %s", release_id) @@ -231,6 +231,7 @@ def create_or_update_release( Post release changelog :param version: The version number :param changelog: The release notes for this version + :return: The status of the request """ log.info("Creating release for %s", tag) @@ -274,6 +275,7 @@ def upload_release_asset( :param release_id: ID of the release to upload to :param file: Path of the file to upload :param label: this parameter has no effect + :return: The status of the request """ url = self.asset_upload_url(release_id) @@ -312,6 +314,7 @@ def upload_dists(self, tag: str, dist_glob: str) -> int: Upload distributions to a release :param tag: Tag to upload for :param path: Path to the dist directory + :return: The number of distributions successfully uploaded """ # Find the release corresponding to this tag diff --git a/semantic_release/hvcs/gitlab.py b/semantic_release/hvcs/gitlab.py index f5452c4ca..1a9437b76 100644 --- a/semantic_release/hvcs/gitlab.py +++ b/semantic_release/hvcs/gitlab.py @@ -106,23 +106,16 @@ def create_release( """ Create a release in a remote VCS, adding any release notes and assets to it - Arguments: - --------- - tag(str): The tag to create the release for - release_notes(str): The changelog description for this version only - prerelease(bool): This parameter has no effect in GitLab - assets(list[str]): A list of paths to files to upload as assets (TODO: not implemented) - noop(bool): If True, do not perform any actions, only log intents - - Returns: - ------- - str: The tag of the release - - Raises: - ------ - GitlabAuthenticationError: If authentication is not correct - GitlabCreateError: If the server cannot perform the request + :param tag: The tag to create the release for + :param release_notes: The changelog description for this version only + :param prerelease: This parameter has no effect in GitLab + :param assets: A list of paths to files to upload as assets (TODO: not implemented) + :param noop: If True, do not perform any actions, only log intents + :return: The tag of the release + + :raises: GitlabAuthenticationError: If authentication is not correct + :raises: GitlabCreateError: If the server cannot perform the request """ if noop: noop_report(f"would have created a release for tag {tag}") @@ -145,20 +138,13 @@ def create_release( @suppress_not_found def get_release_by_tag(self, tag: str) -> gitlab.v4.objects.ProjectRelease | None: """ - Get a release by its tag name - - Arguments: - --------- - tag(str): The tag name to get the release for + Get a release by its tag name. - Returns: - ------- - gitlab.v4.objects.ProjectRelease | None: The release object or None if not found + :param tag: The tag name to get the release for - Raises: - ------ - gitlab.exceptions.GitlabAuthenticationError: If the user is not authenticated + :return: gitlab.v4.objects.ProjectRelease or None if not found + :raises: gitlab.exceptions.GitlabAuthenticationError: If the user is not authenticated """ try: return self.project.releases.get(tag) @@ -175,21 +161,15 @@ def edit_release_notes( # type: ignore[override] release_notes: str, ) -> str: """ - Update the release notes for a given release + Update the release notes for a given release. - Arguments: - --------- - release(gitlab.v4.objects.ProjectRelease): The release object to update - release_notes(str): The new release notes + :param release: The release object to update + :param release_notes: The new release notes - Returns: - ------- - str: The release id + :return: The release id - Raises: - ------ - GitlabAuthenticationError: If authentication is not correct - GitlabUpdateError: If the server cannot perform the request + :raises: GitlabAuthenticationError: If authentication is not correct + :raises: GitlabUpdateError: If the server cannot perform the request """ log.info( @@ -206,24 +186,17 @@ def create_or_update_release( self, tag: str, release_notes: str, prerelease: bool = False ) -> str: """ - Create or update a release for the given tag in a remote VCS - - Arguments: - --------- - tag(str): The tag to create or update the release for - release_notes(str): The changelog description for this version only - prerelease(bool): This parameter has no effect in GitLab + Create or update a release for the given tag in a remote VCS. - Returns: - ------- - str: The release id + :param tag: The tag to create or update the release for + :param release_notes: The changelog description for this version only + :param prerelease: This parameter has no effect in GitLab - Raises: - ------ - ValueError: If the release could not be created or updated - gitlab.exceptions.GitlabAuthenticationError: If the user is not authenticated - GitlabUpdateError: If the server cannot perform the request + :return: The release id + :raises ValueError: If the release could not be created or updated + :raises gitlab.exceptions.GitlabAuthenticationError: If the user is not authenticated + :raises GitlabUpdateError: If the server cannot perform the request """ try: return self.create_release( diff --git a/semantic_release/hvcs/util.py b/semantic_release/hvcs/util.py index fd0d66ebc..3c4f78888 100644 --- a/semantic_release/hvcs/util.py +++ b/semantic_release/hvcs/util.py @@ -21,12 +21,14 @@ def build_requests_session( ) -> Session: """ Create a requests session. + :param raise_for_status: If True, a hook to invoke raise_for_status be installed :param retry: If true, it will use default Retry configuration. if an integer, it - will use default Retry configuration with given integer as total retry - count. if Retry instance, it will use this instance. + will use default Retry configuration with given integer as total retry + count. if Retry instance, it will use this instance. :param auth: Optional TokenAuth instance to be used to provide the Authorization - header to the session + header to the session + :return: configured requests Session """ session = Session() diff --git a/tests/command_line/conftest.py b/tests/command_line/conftest.py index b8160373d..46d68d46e 100644 --- a/tests/command_line/conftest.py +++ b/tests/command_line/conftest.py @@ -6,7 +6,6 @@ from unittest.mock import MagicMock import pytest -from click.testing import CliRunner from requests_mock import ANY from semantic_release.cli import config as cli_config_module @@ -40,11 +39,6 @@ class RetrieveRuntimeContextFn(Protocol): def __call__(self, repo: Repo) -> RuntimeContext: ... -@pytest.fixture -def cli_runner() -> CliRunner: - return CliRunner(mix_stderr=False) - - @pytest.fixture def post_mocker(requests_mock: Mocker) -> Mocker: """Patch all POST requests, mocking a response body for VCS release creation.""" diff --git a/tests/conftest.py b/tests/conftest.py index 3465b236a..911906a7d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING import pytest +from click.testing import CliRunner from git import Commit, Repo from tests.fixtures import * @@ -28,6 +29,11 @@ class TeardownCachedDirFn(Protocol): def __call__(self, directory: Path) -> Path: ... +@pytest.fixture +def cli_runner() -> CliRunner: + return CliRunner(mix_stderr=False) + + @pytest.fixture(scope="session") def default_netrc_username() -> str: return "username" diff --git a/tests/scenario/test_version_stamp.py b/tests/scenario/test_version_stamp.py new file mode 100644 index 000000000..60a408e67 --- /dev/null +++ b/tests/scenario/test_version_stamp.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +from textwrap import dedent +from typing import TYPE_CHECKING + +import pytest +import yaml + +from semantic_release.cli.commands.main import main + +from tests.const import MAIN_PROG_NAME, VERSION_SUBCMD +from tests.fixtures.repos.trunk_based_dev.repo_w_no_tags import ( + repo_with_no_tags_angular_commits, +) +from tests.util import assert_successful_exit_code + +if TYPE_CHECKING: + from click.testing import CliRunner + + from tests.fixtures.example_project import UpdatePyprojectTomlFn + + +@pytest.mark.usefixtures(repo_with_no_tags_angular_commits.__name__) +def test_stamp_version_variables_python( + cli_runner: CliRunner, + update_pyproject_toml: UpdatePyprojectTomlFn, +) -> None: + new_version = "0.1.0" + target_file = Path("src/example/_version.py") + + # Set configuration to modify the python file + update_pyproject_toml( + "tool.semantic_release.version_variables", [f"{target_file}:__version__"] + ) + + # Use the version command and prevent any action besides stamping the version + cli_cmd = [ + MAIN_PROG_NAME, + VERSION_SUBCMD, + "--no-changelog", + "--no-commit", + "--no-tag", + ] + + # Act + result = cli_runner.invoke(main, cli_cmd[1:]) + + # Check the result + assert_successful_exit_code(result, cli_cmd) + + # Load python module for reading the version (ensures the file is valid) + spec = importlib.util.spec_from_file_location("example._version", str(target_file)) + module = importlib.util.module_from_spec(spec) # type: ignore + spec.loader.exec_module(module) # type: ignore + + # Check the version was updated + assert new_version == module.__version__ + + +@pytest.mark.usefixtures(repo_with_no_tags_angular_commits.__name__) +def test_stamp_version_variables_yaml( + cli_runner: CliRunner, + update_pyproject_toml: UpdatePyprojectTomlFn, +) -> None: + orig_version = "0.0.0" + new_version = "0.1.0" + target_file = Path("example.yml") + orig_yaml = dedent( + f"""\ + --- + package: example + version: {orig_version} + date-released: 1970-01-01 + """ + ) + # Write initial text in file + target_file.write_text(orig_yaml) + + # Set configuration to modify the yaml file + update_pyproject_toml( + "tool.semantic_release.version_variables", [f"{target_file}:version"] + ) + + # Use the version command and prevent any action besides stamping the version + cli_cmd = [ + MAIN_PROG_NAME, + VERSION_SUBCMD, + "--no-changelog", + "--no-commit", + "--no-tag", + ] + + # Act + result = cli_runner.invoke(main, cli_cmd[1:]) + + # Check the result + assert_successful_exit_code(result, cli_cmd) + + # Read content + resulting_yaml_obj = yaml.safe_load(target_file.read_text()) + + # Check the version was updated + assert new_version == resulting_yaml_obj["version"] + + # Check the rest of the content is the same (by reseting the version & comparing) + resulting_yaml_obj["version"] = orig_version + + assert yaml.safe_load(orig_yaml) == resulting_yaml_obj + + +@pytest.mark.usefixtures(repo_with_no_tags_angular_commits.__name__) +def test_stamp_version_variables_yaml_cff( + cli_runner: CliRunner, + update_pyproject_toml: UpdatePyprojectTomlFn, +) -> None: + orig_version = "0.0.0" + new_version = "0.1.0" + target_file = Path("CITATION.cff") + # Derived format from python-semantic-release/python-semantic-release#962 + orig_yaml = dedent( + f"""\ + --- + cff-version: 1.2.0 + message: "If you use this software, please cite it as below." + authors: + - family-names: Doe + given-names: Jon + orcid: https://orcid.org/1234-6666-2222-5555 + title: "My Research Software" + version: {orig_version} + date-released: 1970-01-01 + """ + ) + # Write initial text in file + target_file.write_text(orig_yaml) + + # Set configuration to modify the yaml file + update_pyproject_toml( + "tool.semantic_release.version_variables", [f"{target_file}:version"] + ) + + # Use the version command and prevent any action besides stamping the version + cli_cmd = [ + MAIN_PROG_NAME, + VERSION_SUBCMD, + "--no-changelog", + "--no-commit", + "--no-tag", + ] + + # Act + result = cli_runner.invoke(main, cli_cmd[1:]) + + # Check the result + assert_successful_exit_code(result, cli_cmd) + + # Read content + resulting_yaml_obj = yaml.safe_load(target_file.read_text()) + + # Check the version was updated + assert new_version == resulting_yaml_obj["version"] + + # Check the rest of the content is the same (by reseting the version & comparing) + resulting_yaml_obj["version"] = orig_version + + assert yaml.safe_load(orig_yaml) == resulting_yaml_obj + + +@pytest.mark.usefixtures(repo_with_no_tags_angular_commits.__name__) +def test_stamp_version_variables_json( + cli_runner: CliRunner, + update_pyproject_toml: UpdatePyprojectTomlFn, +) -> None: + orig_version = "0.0.0" + new_version = "0.1.0" + target_file = Path("plugins.json") + orig_json = { + "id": "test-plugin", + "version": orig_version, + "meta": { + "description": "Test plugin", + }, + } + # Write initial text in file + target_file.write_text(json.dumps(orig_json, indent=4)) + + # Set configuration to modify the json file + update_pyproject_toml( + "tool.semantic_release.version_variables", [f"{target_file}:version"] + ) + + # Use the version command and prevent any action besides stamping the version + cli_cmd = [ + MAIN_PROG_NAME, + VERSION_SUBCMD, + "--no-changelog", + "--no-commit", + "--no-tag", + ] + + # Act + result = cli_runner.invoke(main, cli_cmd[1:]) + + # Check the result + assert_successful_exit_code(result, cli_cmd) + + # Read content + resulting_json_obj = json.loads(target_file.read_text()) + + # Check the version was updated + assert new_version == resulting_json_obj["version"] + + # Check the rest of the content is the same (by reseting the version & comparing) + resulting_json_obj["version"] = orig_version + + assert orig_json == resulting_json_obj