diff --git a/docs/configuration.rst b/docs/configuration.rst index 7fc4838c4..a94591ff1 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -1340,14 +1340,22 @@ to substitute the version number in the file. The replacement algorithm is **ONL 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``. +The regular expression generated from the ``version_variables`` definition will: - This may become more flexible in the future with resolution of issue `#941`_. +1. Look for the specified ``variable`` name in the ``file``. The variable name can be + enclosed by single (``'``) or double (``"``) quotation marks but they must match. -.. _#941: https://github.com/python-semantic-release/python-semantic-release/issues/941 +2. The variable name defined by ``variable`` and the version must be separated by + an operand symbol (``=``, ``:``, ``:=``, or ``@``). Whitespace is optional around + the symbol. + +3. The value of the variable must match a `SemVer`_ regular expression and can be + enclosed by single (``'``) or double (``"``) quotation marks but they must match. However, + the enclosing quotes of the value do not have to match the quotes surrounding the variable + name. + +4. If the format type is set to ``tf`` then the variable value must have the matching prefix + and suffix of the :ref:`config-tag_format` setting around the `SemVer`_ version number. Given the pattern matching nature of this feature, the Regular Expression is able to support most file formats because of the similarity of variable declaration across @@ -1360,6 +1368,47 @@ regardless of file extension because it looks for a matching pattern string. TOML files as it actually will interpret the TOML file and replace the version number before writing the file back to disk. +This is a comprehensive list (but not all variations) of examples where the following versions +will be matched and replaced by the new version: + +.. code-block:: + + # Common variable declaration formats + version='1.2.3' + version = "1.2.3" + release = "v1.2.3" # if tag_format is set + + # YAML + version: 1.2.3 + + # JSON + "version": "1.2.3" + + # NPM & GitHub Actions YAML + version@1.2.3 + version@v1.2.3 # if tag_format is set + + # Walrus Operator + version := "1.2.3" + + # Excessive whitespace + version = '1.2.3' + + # Mixed Quotes + "version" = '1.2.3' + + # Custom Tag Format with tag_format set (monorepos) + __release__ = "module-v1.2.3" + +.. 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 + .. 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 diff --git a/src/semantic_release/version/declaration.py b/src/semantic_release/version/declaration.py index 92da001a1..3c225d1b5 100644 --- a/src/semantic_release/version/declaration.py +++ b/src/semantic_release/version/declaration.py @@ -14,8 +14,6 @@ from semantic_release.version.declarations.toml import TomlVersionDeclaration if TYPE_CHECKING: # pragma: no cover - from typing import Any - from semantic_release.version.version import Version @@ -66,13 +64,8 @@ def content(self) -> str: self._content = self.path.read_text() return self._content - # mypy doesn't like properties? - @content.setter # type: ignore[attr-defined] - def _(self, _: Any) -> None: - raise AttributeError("'content' cannot be set directly") - - @content.deleter # type: ignore[attr-defined] - def _(self) -> None: + @content.deleter + def content(self) -> None: log.debug("resetting instance-stored source file contents") self._content = None diff --git a/src/semantic_release/version/declarations/pattern.py b/src/semantic_release/version/declarations/pattern.py index 73f67b465..55873ce0a 100644 --- a/src/semantic_release/version/declarations/pattern.py +++ b/src/semantic_release/version/declarations/pattern.py @@ -230,9 +230,9 @@ def from_string_definition( # Supports optional matching quotations around variable name # Negative lookbehind to ensure we don't match part of a variable name f"""(?x)(?P['"])?(?['"])?{value_replace_pattern_str}(?P=quote2)?""", ], diff --git a/tests/e2e/cmd_version/test_version_stamp.py b/tests/e2e/cmd_version/test_version_stamp.py index c63d5b66c..9d45b6019 100644 --- a/tests/e2e/cmd_version/test_version_stamp.py +++ b/tests/e2e/cmd_version/test_version_stamp.py @@ -373,6 +373,78 @@ def test_stamp_version_variables_json( assert orig_json == resulting_json_obj +@pytest.mark.usefixtures(repo_w_no_tags_conventional_commits.__name__) +def test_stamp_version_variables_yaml_github_actions( + cli_runner: CliRunner, + update_pyproject_toml: UpdatePyprojectTomlFn, + default_tag_format_str: str, +) -> None: + """ + Given a yaml file with github actions 'uses:' directives which use @vX.Y.Z version declarations, + When a version is stamped and configured to stamp the version using the tag format, + Then the file is updated with the new version in the tag format + + Based on https://github.com/python-semantic-release/python-semantic-release/issues/1156 + """ + orig_version = "0.0.0" + new_version = "0.1.0" + target_file = Path("combined.yml") + action1_yaml_filepath = "my-org/my-actions/.github/workflows/action1.yml" + action2_yaml_filepath = "my-org/my-actions/.github/workflows/action2.yml" + orig_yaml = dedent( + f"""\ + --- + on: + workflow_call: + + jobs: + action1: + uses: {action1_yaml_filepath}@{default_tag_format_str.format(version=orig_version)} + action2: + uses: {action2_yaml_filepath}@{default_tag_format_str.format(version=orig_version)} + """ + ) + expected_action1_value = ( + f"{action1_yaml_filepath}@{default_tag_format_str.format(version=new_version)}" + ) + expected_action2_value = ( + f"{action2_yaml_filepath}@{default_tag_format_str.format(version=new_version)}" + ) + + # Setup: Write initial text in file + target_file.write_text(orig_yaml) + + # Setup: Set configuration to modify the yaml file + update_pyproject_toml( + "tool.semantic_release.version_variables", + [ + f"{target_file}:{action1_yaml_filepath}:{VersionStampType.TAG_FORMAT.value}", + f"{target_file}:{action2_yaml_filepath}:{VersionStampType.TAG_FORMAT.value}", + ], + ) + + # Act + cli_cmd = VERSION_STAMP_CMD + 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 expected_action1_value == resulting_yaml_obj["jobs"]["action1"]["uses"] + assert expected_action2_value == resulting_yaml_obj["jobs"]["action2"]["uses"] + + # Check the rest of the content is the same (by setting the version & comparing) + original_yaml_obj = yaml.safe_load(orig_yaml) + original_yaml_obj["jobs"]["action1"]["uses"] = expected_action1_value + original_yaml_obj["jobs"]["action2"]["uses"] = expected_action2_value + + assert original_yaml_obj == resulting_yaml_obj + + @pytest.mark.usefixtures(repo_w_no_tags_conventional_commits.__name__) def test_stamp_version_variables_yaml_kustomization_container_spec( cli_runner: CliRunner, diff --git a/tests/unit/semantic_release/version/declarations/test_pattern_declaration.py b/tests/unit/semantic_release/version/declarations/test_pattern_declaration.py index b49f87fa0..fd7cb7dad 100644 --- a/tests/unit/semantic_release/version/declarations/test_pattern_declaration.py +++ b/tests/unit/semantic_release/version/declarations/test_pattern_declaration.py @@ -92,6 +92,24 @@ def test_pattern_declaration_is_version_replacer(): '''__version__ = "module-v1.0.0"''', f'''__version__ = "module-v{next_version}"''', ), + ( + # Based on https://github.com/python-semantic-release/python-semantic-release/issues/1156 + "Using default tag format for github actions uses-directive", + f"{test_file}:repo/action-name:{VersionStampType.TAG_FORMAT.value}", + lazy_fixture(default_tag_format_str.__name__), + # Uses @ symbol separator without quotes or spaces + """ uses: repo/action-name@v1.0.0""", + f""" uses: repo/action-name@v{next_version}""", + ), + ( + # Based on https://github.com/python-semantic-release/python-semantic-release/issues/1156 + "Using custom tag format for github actions uses-directive", + f"{test_file}:repo/action-name:{VersionStampType.TAG_FORMAT.value}", + "module-v{version}", + # Uses @ symbol separator without quotes or spaces + """ uses: repo/action-name@module-v1.0.0""", + f""" uses: repo/action-name@module-v{next_version}""", + ), ( # Based on https://github.com/python-semantic-release/python-semantic-release/issues/846 "Using default tag format for multi-line yaml", @@ -205,7 +223,7 @@ def test_pattern_declaration_from_definition( When update_file_w_version() is called with a new version, Then the file is updated with the new version string in the specified tag or number format - Version variables can be separated by either "=", ":", or ':=' with optional whitespace + Version variables can be separated by either "=", ":", "@", or ':=' with optional whitespace between operator and variable name. The variable name or values can also be wrapped in either single or double quotes. """