python-semantic-release 10.5.2__py3-none-any.whl → 10.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-semantic-release
3
- Version: 10.5.2
3
+ Version: 10.6.0
4
4
  Summary: Automatic Semantic Versioning for Python projects
5
5
  Author-email: Rolf Erik Lekang <me@rolflekang.com>, codejedi365 <codejedi365@gmail.com>
6
6
  License: MIT
7
- Project-URL: changelog, https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md
7
+ Project-URL: changelog, https://python-semantic-release.readthedocs.io/en/stable/misc/psr_changelog.html
8
8
  Project-URL: documentation, https://python-semantic-release.readthedocs.io
9
9
  Project-URL: homepage, https://python-semantic-release.readthedocs.io
10
10
  Project-URL: issues, https://github.com/python-semantic-release/python-semantic-release/issues
@@ -1,12 +1,12 @@
1
- python_semantic_release-10.5.2.dist-info/licenses/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
1
+ python_semantic_release-10.6.0.dist-info/licenses/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
2
2
  semantic_release/__init__.py,sha256=tRJWhrn_dUt0QycXD2DoJSfEP5uwmxngH7jvbG2i-hA,1317
3
3
  semantic_release/__main__.py,sha256=pksxr6g1vkKq98Q1lShsxG8tk55IMiSMHzAHKyFU5x0,1704
4
4
  semantic_release/const.py,sha256=wInJR7vcOgT1ysm5VuJQ6lD_ZGYnCwRVKz7Uz3htQc4,861
5
5
  semantic_release/enums.py,sha256=vrEw1UNRcNrFjPqOFnuUzfeoqKj0ChixVVlyk5fqbng,1744
6
6
  semantic_release/errors.py,sha256=FyocaqHbRhux-iNmCf9nI7awyUaGKjG9_5C_QDvhEas,3399
7
- semantic_release/gitproject.py,sha256=qF4GVZh-aaS4bVV3OmFQLmXMAxHNFyZCwUaWPtUmZ1k,16546
7
+ semantic_release/gitproject.py,sha256=18jsD8YFsaKKqt_l5a0ccygIE_sOqvqf9FoEkJjyCkk,18584
8
8
  semantic_release/globals.py,sha256=IBhBbhZr2jx8dmpySnnu9m9jOGYu9Yu-vqHvAGQxgnw,464
9
- semantic_release/helpers.py,sha256=2uQXOiuiemiviVD52SH2FF6cn14p_gqvREeHvP7dexw,10012
9
+ semantic_release/helpers.py,sha256=3munzjFYx_wxi97tRjCoP7EhcMtbgnO7G0ENlShCm8w,10334
10
10
  semantic_release/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  semantic_release/changelog/__init__.py,sha256=Bg6Xe5Vt32rWoMscW-hd4sUwiZqzWmsg4CD1EhMesMY,262
12
12
  semantic_release/changelog/context.py,sha256=234LLcB_uuGaQ5N_zTbcQYF27ZwYIBmicIWkhFs7-aQ,5993
@@ -15,28 +15,28 @@ semantic_release/changelog/template.py,sha256=eqOjtVfBbksgTK4CAZOajfkaAcKueJ-FhF
15
15
  semantic_release/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  semantic_release/cli/changelog_writer.py,sha256=2jP5b0LK61Y2tb22GQSFFmwHwXd2WjbopgsMAmsQsfY,9284
17
17
  semantic_release/cli/cli_context.py,sha256=Nop71LdVCJOeSUHgTXunMyK3xAu_QKQC2cRp1QBVkX0,4134
18
- semantic_release/cli/config.py,sha256=zNXlJHZBnFk2GEemgCg2Yw42EWihtFMot1twI7bEN-g,33668
18
+ semantic_release/cli/config.py,sha256=jfG_N4rUo9pMpoC1eHNku_fK47eQXPrzRENu2g8CwWg,34297
19
19
  semantic_release/cli/const.py,sha256=h7XE2D0D__TAZSrUUtVszwvzpkHTMOiQCf97XQNbEvA,163
20
20
  semantic_release/cli/github_actions_output.py,sha256=yC0nsMvEFGACjDwB8DdmGKwNGI8aIIhDxRHrmcS7tzA,5410
21
21
  semantic_release/cli/masking_filter.py,sha256=7t7XFL7Iy4QTvaYevZ-jnTkJBz15GoBw1vjW3hihe38,3159
22
- semantic_release/cli/util.py,sha256=4rf4xDHb7l-XpdcFNtslFz1xHslnBu6cTgWHXVnQXQs,3746
22
+ semantic_release/cli/util.py,sha256=6TAbHgDZU6T-rm5yzg2TdGP_-ueHQhfbAkghP1q6we0,3750
23
23
  semantic_release/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  semantic_release/cli/commands/changelog.py,sha256=wJfd4VVfrGnu2jnIpG25cdVcbXIX-ElKl3b2jdtPPa0,5391
25
- semantic_release/cli/commands/generate_config.py,sha256=2xZOu3NpyhBp0pWr7d8ugKl_kjqQgpSsSMHq5wHTfrE,1699
25
+ semantic_release/cli/commands/generate_config.py,sha256=sKTO2V_XGRFc6M5KaTEPS4UOmqDuQOyTm64IYLr1VBc,2452
26
26
  semantic_release/cli/commands/main.py,sha256=u1zhkkvKCZ2TtUqjzvdFTe5UZsvfws_pjqqo6CY0bBo,4351
27
- semantic_release/cli/commands/publish.py,sha256=CE_LJTxFnc337MfpsfdJopi7QCwwE13GqGNQ-dNgWis,2871
28
- semantic_release/cli/commands/version.py,sha256=1zJ96_WSQh4bY_75P3eTJTfwrEZcJGHf7XJO2yA66SA,29459
27
+ semantic_release/cli/commands/publish.py,sha256=q9sNYCqlIjbT8RjJvHPVYzDdAjtnXe4_IdT0_h4S9MA,3047
28
+ semantic_release/cli/commands/version.py,sha256=8pn21AskKRMg1WJJXNNTZzlS1bNZefHKOPDRe-3NF5M,29502
29
29
  semantic_release/commit_parser/__init__.py,sha256=uCC2YI-EDPUAa6TwWYJA8TS7baWE2bXrLEGcrhJKofI,1323
30
30
  semantic_release/commit_parser/_base.py,sha256=DLsHnbXG-39JkUbcnsBCSV2GmV35w1rasyoMhK8G0UE,3058
31
31
  semantic_release/commit_parser/angular.py,sha256=MY_fo9F4EZ-ac8wYzBR0uD94O5Li2D-8zEMR01wss4c,18534
32
- semantic_release/commit_parser/emoji.py,sha256=0VecUMMcj43UaKDZ2GaFFUG26zOJAvXsEobTHaXNkk0,17749
32
+ semantic_release/commit_parser/emoji.py,sha256=IMRq0sUat-cU7aNIg38M0kAmZ376p7zuRy3XSOgsrOM,17826
33
33
  semantic_release/commit_parser/scipy.py,sha256=gA9TfmrxGVvtv6kki6N_9abJx-_WlpBsSX2TBh0YgPw,19463
34
34
  semantic_release/commit_parser/tag.py,sha256=bVO2XghM0G_eW2rG9Xc2q5TPsjtxr-xcHK5RpE1u_HM,3537
35
35
  semantic_release/commit_parser/token.py,sha256=1_q8mJ4SRu7kNfa-Nxr8fEyuvCfjPgiPEitqSP1KR5g,7904
36
36
  semantic_release/commit_parser/util.py,sha256=hcLjc16o7l6y_5Hfl8IxmF4ijaJD62JdjdB2DJWAe-g,3959
37
37
  semantic_release/commit_parser/conventional/__init__.py,sha256=JHPL3S6trM4Le9MXnXc6iPYxxQnE4j0LG83_BWIomOw,602
38
38
  semantic_release/commit_parser/conventional/options.py,sha256=AnRgTRMF-3X5QonTqq6bqJEbmGgQBzppyRZ79hct28g,2598
39
- semantic_release/commit_parser/conventional/options_monorepo.py,sha256=7OCpuaYhw01bfoffTGfPgEWzRRyVsdsq_n2eo8Dt8-8,3210
39
+ semantic_release/commit_parser/conventional/options_monorepo.py,sha256=ZEKhhsHbP8fgg0XSluZK27itbNkznslkCkyTi6aQJ3Q,3485
40
40
  semantic_release/commit_parser/conventional/parser.py,sha256=Pex26cvMqAKx9NPH3KROYg7k76R0Nsbs4aHor5SbMZg,16671
41
41
  semantic_release/commit_parser/conventional/parser_monorepo.py,sha256=nVAc_gK4tJ5UM_rd9KbXgBWeBgrI_q2-vav3_4748r8,21189
42
42
  semantic_release/data/templates/conventional/md/.release_notes.md.j2,sha256=DlMVAJMGqE27TwJ-2kviYaFhd3uWqXiU6Ikl15Ukne8,2512
@@ -62,23 +62,24 @@ semantic_release/hvcs/__init__.py,sha256=JwoaLOF-12L-OBo_9-tOXXhdiHKeVungA9865to
62
62
  semantic_release/hvcs/_base.py,sha256=gUCAbSHidhB6ou1oOP--wqEaVc4X2NPLjZQ5P_g3Bwc,2600
63
63
  semantic_release/hvcs/bitbucket.py,sha256=dbWvqf79ACN42srhfVRtJ8m5WNI-_d7NE1cxZPwtCLs,10148
64
64
  semantic_release/hvcs/gitea.py,sha256=PWK3URTuiWvx0SwSlSIGCbtVmD7XjaYo051fiTUYsJY,13463
65
- semantic_release/hvcs/github.py,sha256=guXx7Zc2o3z2eOs8KzYt3BifO5cNWuti1ZmZdL7bN3Q,20695
65
+ semantic_release/hvcs/github.py,sha256=taaoLs1khLlpJp3IjyRiNBk_c_bQim8goYSistnL_Xk,21157
66
66
  semantic_release/hvcs/gitlab.py,sha256=uUnTKq9Kpqc83ppXA3NXfVYnz4O1ej2noz68jCYzg68,10606
67
67
  semantic_release/hvcs/remote_hvcs_base.py,sha256=QtkjdMy9l-c7UOtyPz25cqVOkCk4IU-EOw6A8lP8l-U,6327
68
68
  semantic_release/hvcs/token_auth.py,sha256=ZjT56-NIPB4OKIt1qwHCu1TavXnrWFIBl9ARlg56hgU,663
69
69
  semantic_release/hvcs/util.py,sha256=PUNV4yUlpzDtNCFmh2joaPdU4JyfUBnVp0zaQsT9EDQ,2871
70
70
  semantic_release/version/__init__.py,sha256=CLhtGQry9dLIij5XyRa9ZevxU_1p8tjMTSQ-K_GMpWM,270
71
71
  semantic_release/version/algorithm.py,sha256=IxgYNF78W7qdMzdV4WsoljwfJgB-sn2XdfkevD0aZlo,16254
72
- semantic_release/version/declaration.py,sha256=eot_lUyFaEhzK4bPncfv9tahf51LdxZP6EaS54h3aAs,3635
72
+ semantic_release/version/declaration.py,sha256=qWHnDm8xakX_dXcPDhKk_3J5Zuu7HUYKmbqm5Iv68uQ,3743
73
73
  semantic_release/version/translator.py,sha256=iIfu3WreB9qqPHgqJLILbBluVQQNcpP0DEsnn_WzAaM,3689
74
74
  semantic_release/version/version.py,sha256=Y3Qqqv7CypirikT7jNqqFMNAvR2UjV-VQx-c2G0mZc0,14519
75
75
  semantic_release/version/declarations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
76
  semantic_release/version/declarations/enum.py,sha256=3n5Py9DoFkmItIdsmtQrJgmAhepTv_1EnogzSdXu1Wg,244
77
- semantic_release/version/declarations/i_version_replacer.py,sha256=oP6BxJuxwI44roI6448tomShv1sMoy9ry8TlhhIQtfc,2416
77
+ semantic_release/version/declarations/file.py,sha256=m-MohBTN69T3unoPcSBgwA76ftwPr2WXfGgVjSvf1vY,4683
78
+ semantic_release/version/declarations/i_version_replacer.py,sha256=YktOY_7cfguZHEyV5f_R-ldMznj140mcS0BXdoa9zf8,2584
78
79
  semantic_release/version/declarations/pattern.py,sha256=sKk0uQpJjWVZc8RJUjxQoEPUvFLxXNGGBow5h1IqCTM,8378
79
80
  semantic_release/version/declarations/toml.py,sha256=2K4DtX5Qq1iHT8cG8mISPTMmp50w6Av0KmLAKZPYqq8,4931
80
- python_semantic_release-10.5.2.dist-info/METADATA,sha256=RMojFB6HaaUcQbydFl05KFC6aIj2OhRVxHRRVeqZrqI,4064
81
- python_semantic_release-10.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
82
- python_semantic_release-10.5.2.dist-info/entry_points.txt,sha256=kzkCyDJsMOwgpFwEWKE9wxN1tXaUP6g6GIO4xtc0QuE,162
83
- python_semantic_release-10.5.2.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
84
- python_semantic_release-10.5.2.dist-info/RECORD,,
81
+ python_semantic_release-10.6.0.dist-info/METADATA,sha256=loZ9-00JeyoQIE0DUn9O3JlLMhbpDVX8AHW6ULb_G6c,4053
82
+ python_semantic_release-10.6.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
83
+ python_semantic_release-10.6.0.dist-info/entry_points.txt,sha256=kzkCyDJsMOwgpFwEWKE9wxN1tXaUP6g6GIO4xtc0QuE,162
84
+ python_semantic_release-10.6.0.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
85
+ python_semantic_release-10.6.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import sys
5
+ from typing import Literal
4
6
 
5
7
  import click
6
8
  import tomlkit
@@ -31,7 +33,9 @@ from semantic_release.cli.config import RawConfig
31
33
  "'semantic_release'"
32
34
  ),
33
35
  )
34
- def generate_config(fmt: str = "toml", is_pyproject_toml: bool = False) -> None:
36
+ def generate_config(
37
+ fmt: Literal["toml", "json"], is_pyproject_toml: bool = False
38
+ ) -> None:
35
39
  """
36
40
  Generate default configuration for semantic-release, to help you get started
37
41
  quickly. You can inspect the defaults, write to a file and then edit according to
@@ -42,14 +46,29 @@ def generate_config(fmt: str = "toml", is_pyproject_toml: bool = False) -> None:
42
46
  """
43
47
  # due to possible IntEnum values (which are not supported by tomlkit.dumps, see sdispater/tomlkit#237),
44
48
  # we must ensure the transformation of the model to a dict uses json serializable values
45
- config = RawConfig().model_dump(mode="json", exclude_none=True)
49
+ config_dct = {
50
+ "semantic_release": RawConfig().model_dump(mode="json", exclude_none=True)
51
+ }
46
52
 
47
- config_dct = {"semantic_release": config}
48
- if is_pyproject_toml and fmt == "toml":
49
- config_dct = {"tool": config_dct}
53
+ if is_pyproject_toml:
54
+ output = tomlkit.dumps({"tool": config_dct})
50
55
 
51
- if fmt == "toml":
52
- click.echo(tomlkit.dumps(config_dct))
56
+ elif fmt == "toml":
57
+ output = tomlkit.dumps(config_dct)
53
58
 
54
59
  elif fmt == "json":
55
- click.echo(json.dumps(config_dct, indent=4))
60
+ output = json.dumps(config_dct, indent=4)
61
+
62
+ else:
63
+ raise ValueError(f"Unsupported format: {fmt}")
64
+
65
+ # Write output directly to stdout buffer as UTF-8 bytes
66
+ # This ensures consistent UTF-8 output on all platforms, especially Windows where
67
+ # shell redirection (>, >>) defaults to the system encoding (e.g., UTF-16LE or cp1252)
68
+ # By writing to sys.stdout.buffer, we bypass the encoding layer and guarantee UTF-8.
69
+ try:
70
+ sys.stdout.buffer.write(f"{output.strip()}\n".encode("utf-8")) # noqa: UP012; allow explicit encoding declaration
71
+ sys.stdout.buffer.flush()
72
+ except (AttributeError, TypeError):
73
+ # Fallback for environments without buffer (shouldn't happen in standard Python)
74
+ click.echo(output)
@@ -6,6 +6,7 @@ import click
6
6
  from git import Repo
7
7
 
8
8
  from semantic_release.cli.util import noop_report
9
+ from semantic_release.errors import AssetUploadError
9
10
  from semantic_release.globals import logger
10
11
  from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase
11
12
  from semantic_release.version.algorithm import tags_and_versions
@@ -90,9 +91,13 @@ def publish(cli_ctx: CliContextObj, tag: str) -> None:
90
91
  )
91
92
  return
92
93
 
93
- publish_distributions(
94
- tag=tag,
95
- hvcs_client=hvcs_client,
96
- dist_glob_patterns=dist_glob_patterns,
97
- noop=runtime.global_cli_options.noop,
98
- )
94
+ try:
95
+ publish_distributions(
96
+ tag=tag,
97
+ hvcs_client=hvcs_client,
98
+ dist_glob_patterns=dist_glob_patterns,
99
+ noop=runtime.global_cli_options.noop,
100
+ )
101
+ except AssetUploadError as err:
102
+ click.echo(err, err=True)
103
+ ctx.exit(1)
@@ -753,6 +753,7 @@ def version( # noqa: C901
753
753
  project.verify_upstream_unchanged(
754
754
  local_ref="HEAD~1",
755
755
  upstream_ref=config.remote.name,
756
+ remote_url=remote_url,
756
757
  noop=opts.noop,
757
758
  )
758
759
  except UpstreamBranchChangedError as exc:
@@ -57,6 +57,7 @@ from semantic_release.errors import (
57
57
  )
58
58
  from semantic_release.globals import logger
59
59
  from semantic_release.helpers import dynamic_import
60
+ from semantic_release.version.declarations.file import FileVersionDeclaration
60
61
  from semantic_release.version.declarations.i_version_replacer import IVersionReplacer
61
62
  from semantic_release.version.declarations.pattern import PatternVersionDeclaration
62
63
  from semantic_release.version.declarations.toml import TomlVersionDeclaration
@@ -757,12 +758,22 @@ class RuntimeContext:
757
758
  ) from err
758
759
 
759
760
  try:
760
- version_declarations.extend(
761
- PatternVersionDeclaration.from_string_definition(
762
- definition, raw.tag_format
761
+ for definition in iter(raw.version_variables or ()):
762
+ # Check if this is a file replacement definition (pattern is "*")
763
+ parts = definition.split(":", maxsplit=2)
764
+ if len(parts) >= 2 and parts[1] == "*":
765
+ # Use FileVersionDeclaration for entire file replacement
766
+ version_declarations.append(
767
+ FileVersionDeclaration.from_string_definition(definition)
768
+ )
769
+ continue
770
+
771
+ # Use PatternVersionDeclaration for pattern-based replacement
772
+ version_declarations.append(
773
+ PatternVersionDeclaration.from_string_definition(
774
+ definition, raw.tag_format
775
+ )
763
776
  )
764
- for definition in iter(raw.version_variables or ())
765
- )
766
777
  except ValueError as err:
767
778
  raise InvalidConfiguration(
768
779
  str.join(
@@ -75,7 +75,7 @@ def load_raw_config_file(config_file: Path | str) -> dict[Any, Any]:
75
75
  while trying to read the specified configuration file
76
76
  """
77
77
  logger.info("Loading configuration from %s", config_file)
78
- raw_text = (Path() / config_file).resolve().read_text(encoding="utf-8")
78
+ raw_text = (Path() / config_file).resolve().read_text(encoding="utf-8-sig")
79
79
  try:
80
80
  logger.debug("Trying to parse configuration %s in TOML format", config_file)
81
81
  return parse_toml(raw_text)
@@ -43,37 +43,46 @@ class ConventionalCommitMonorepoParserOptions(ConventionalCommitParserOptions):
43
43
  to match them literally.
44
44
  """
45
45
 
46
- @classmethod
47
46
  @field_validator("path_filters", mode="before")
48
- def convert_strs_to_paths(cls, value: Any) -> tuple[Path, ...]:
49
- values = value if isinstance(value, Iterable) else [value]
50
- results: list[Path] = []
47
+ @classmethod
48
+ def convert_strs_to_paths(cls, value: Any) -> tuple[str, ...]:
49
+ if isinstance(value, str):
50
+ return (value,)
51
51
 
52
- for val in values:
53
- if isinstance(val, (str, Path)):
54
- results.append(Path(val))
55
- continue
52
+ if isinstance(value, Path):
53
+ return (str(value),)
56
54
 
57
- raise TypeError(f"Invalid type: {type(val)}, expected str or Path.")
55
+ if isinstance(value, Iterable):
56
+ results: list[str] = []
57
+ for val in value:
58
+ if isinstance(val, (str, Path)):
59
+ results.append(str(Path(val)))
60
+ continue
58
61
 
59
- return tuple(results)
62
+ msg = f"Invalid type: {type(val)}, expected str or Path."
63
+ raise TypeError(msg)
64
+
65
+ return tuple(results)
66
+
67
+ msg = f"Invalid type: {type(value)}, expected str, Path, or Iterable."
68
+ raise TypeError(msg)
60
69
 
61
- @classmethod
62
70
  @field_validator("path_filters", mode="after")
63
- def resolve_path(cls, dir_paths: tuple[Path, ...]) -> tuple[Path, ...]:
71
+ @classmethod
72
+ def resolve_path(cls, dir_path_strs: tuple[str, ...]) -> tuple[str, ...]:
64
73
  return tuple(
65
74
  (
66
- Path(f"!{Path(str_path[1:]).expanduser().absolute().resolve()}")
75
+ f"!{Path(str_path[1:]).expanduser().absolute().resolve()}"
67
76
  # maintains the negation prefix if it exists
68
- if (str_path := str(path)).startswith("!")
77
+ if str_path.startswith("!")
69
78
  # otherwise, resolve the path normally
70
- else path.expanduser().absolute().resolve()
79
+ else str(Path(str_path).expanduser().absolute().resolve())
71
80
  )
72
- for path in dir_paths
81
+ for str_path in dir_path_strs
73
82
  )
74
83
 
75
- @classmethod
76
84
  @field_validator("scope_prefix", mode="after")
85
+ @classmethod
77
86
  def validate_scope_prefix(cls, scope_prefix: str) -> str:
78
87
  if not scope_prefix:
79
88
  return ""
@@ -65,7 +65,12 @@ class EmojiParserOptions(ParserOptions):
65
65
  )
66
66
  """Commit-type prefixes that should result in a patch release bump."""
67
67
 
68
- other_allowed_tags: Tuple[str, ...] = (":memo:", ":checkmark:")
68
+ other_allowed_tags: Tuple[str, ...] = (
69
+ ":checkmark:",
70
+ ":construction_worker:",
71
+ ":memo:",
72
+ ":recycle:",
73
+ )
69
74
  """Commit-type prefixes that are allowed but do not result in a version bump."""
70
75
 
71
76
  allowed_tags: Tuple[str, ...] = (
@@ -336,13 +336,18 @@ class GitProject:
336
336
  raise GitPushError(f"Failed to push tag ({tag}) to remote") from err
337
337
 
338
338
  def verify_upstream_unchanged( # noqa: C901
339
- self, local_ref: str = "HEAD", upstream_ref: str = "origin", noop: bool = False
339
+ self,
340
+ local_ref: str = "HEAD",
341
+ upstream_ref: str = "origin",
342
+ remote_url: str | None = None,
343
+ noop: bool = False,
340
344
  ) -> None:
341
345
  """
342
346
  Verify that the upstream branch has not changed since the given local reference.
343
347
 
344
348
  :param local_ref: The local reference to compare against upstream (default: HEAD)
345
349
  :param upstream_ref: The name of the upstream remote or specific remote branch (default: origin)
350
+ :param remote_url: Optional authenticated remote URL to use for fetching (default: None, uses configured remote)
346
351
  :param noop: Whether to skip the actual verification (for dry-run mode)
347
352
 
348
353
  :raises UpstreamBranchChangedError: If the upstream branch has changed
@@ -409,7 +414,46 @@ class GitProject:
409
414
  # Fetch the latest changes from the remote
410
415
  self.logger.info("Fetching latest changes from remote '%s'", remote_name)
411
416
  try:
412
- remote_ref_obj.fetch()
417
+ # Check if we should use authenticated URL for fetch
418
+ # Only use remote_url if:
419
+ # 1. It's provided and different from the configured remote URL
420
+ # 2. It contains authentication credentials (@ symbol)
421
+ # 3. The configured remote is NOT a local path, file:// URL, or test URL (example.com)
422
+ # This ensures we don't break tests or local development
423
+ configured_url = remote_ref_obj.url
424
+ is_local_or_test_remote = (
425
+ configured_url.startswith(("file://", "/", "C:/", "H:/"))
426
+ or "example.com" in configured_url
427
+ or not configured_url.startswith(
428
+ (
429
+ "https://",
430
+ "http://",
431
+ "git://",
432
+ "git@",
433
+ "ssh://",
434
+ "git+ssh://",
435
+ )
436
+ )
437
+ )
438
+
439
+ use_authenticated_fetch = (
440
+ remote_url
441
+ and "@" in remote_url
442
+ and remote_url != configured_url
443
+ and not is_local_or_test_remote
444
+ )
445
+
446
+ if use_authenticated_fetch:
447
+ # Use authenticated remote URL for fetch
448
+ # Fetch the remote branch and update the local tracking ref
449
+ repo.git.fetch(
450
+ remote_url,
451
+ f"refs/heads/{remote_branch_name}:refs/remotes/{upstream_full_ref_name}",
452
+ )
453
+ else:
454
+ # Use the default remote configuration for local paths,
455
+ # file:// URLs, test URLs, or when no authentication is needed
456
+ remote_ref_obj.fetch()
413
457
  except GitCommandError as err:
414
458
  self.logger.exception(str(err))
415
459
  err_msg = f"Failed to fetch from remote '{remote_name}'"
@@ -9,7 +9,7 @@ from functools import lru_cache, reduce, wraps
9
9
  from pathlib import Path, PurePosixPath
10
10
  from re import IGNORECASE, compile as regexp
11
11
  from typing import TYPE_CHECKING, Any, Callable, NamedTuple, Sequence, TypeVar
12
- from urllib.parse import urlsplit
12
+ from urllib.parse import urlsplit, urlunsplit
13
13
 
14
14
  from semantic_release.globals import logger
15
15
 
@@ -215,6 +215,16 @@ class ParsedGitUrl(NamedTuple):
215
215
  repo_name: str
216
216
 
217
217
 
218
+ def _hide_credentials_in_url(url: str) -> str:
219
+ url_parts = urlsplit(url)
220
+
221
+ if not url_parts.scheme or "@" not in url_parts.netloc:
222
+ return url
223
+
224
+ _, _, host = url_parts.netloc.rpartition("@")
225
+ return urlunsplit(url_parts._replace(netloc=f"<credentials>@{host}"))
226
+
227
+
218
228
  @lru_cache(maxsize=512)
219
229
  def parse_git_url(url: str) -> ParsedGitUrl:
220
230
  """
@@ -242,7 +252,7 @@ def parse_git_url(url: str) -> ParsedGitUrl:
242
252
 
243
253
  Raises ValueError if the url can't be parsed.
244
254
  """
245
- logger.debug("Parsing git url %r", url)
255
+ logger.debug("Parsing git url %r", _hide_credentials_in_url(url))
246
256
 
247
257
  # Normalizers are a list of tuples of (pattern, replacement)
248
258
  normalizers = [
@@ -463,14 +463,25 @@ class Github(RemoteHvcsBase):
463
463
 
464
464
  # Upload assets
465
465
  n_succeeded = 0
466
+ errors = []
466
467
  for file_path in (
467
468
  f for f in glob.glob(dist_glob, recursive=True) if os.path.isfile(f)
468
469
  ):
469
470
  try:
470
471
  self.upload_release_asset(release_id, file_path)
471
472
  n_succeeded += 1
472
- except HTTPError: # noqa: PERF203
473
+ except HTTPError as err: # noqa: PERF203
473
474
  logger.exception("error uploading asset %s", file_path)
475
+ status_code = (
476
+ err.response.status_code if err.response is not None else "unknown"
477
+ )
478
+ error_msg = f"Failed to upload asset '{file_path}' to release"
479
+ if status_code != "unknown":
480
+ error_msg += f" (HTTP {status_code})"
481
+ errors.append(error_msg)
482
+
483
+ if errors:
484
+ raise AssetUploadError("\n".join(errors))
474
485
 
475
486
  return n_succeeded
476
487
 
@@ -9,6 +9,7 @@ from deprecated.sphinx import deprecated
9
9
 
10
10
  from semantic_release.globals import logger
11
11
  from semantic_release.version.declarations.enum import VersionStampType
12
+ from semantic_release.version.declarations.file import FileVersionDeclaration
12
13
  from semantic_release.version.declarations.i_version_replacer import IVersionReplacer
13
14
  from semantic_release.version.declarations.pattern import PatternVersionDeclaration
14
15
  from semantic_release.version.declarations.toml import TomlVersionDeclaration
@@ -19,11 +20,12 @@ if TYPE_CHECKING: # pragma: no cover
19
20
 
20
21
  # Globals
21
22
  __all__ = [
23
+ "FileVersionDeclaration",
22
24
  "IVersionReplacer",
23
- "VersionStampType",
24
25
  "PatternVersionDeclaration",
25
26
  "TomlVersionDeclaration",
26
27
  "VersionDeclarationABC",
28
+ "VersionStampType",
27
29
  ]
28
30
 
29
31
 
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ from deprecated.sphinx import deprecated
7
+
8
+ from semantic_release.globals import logger
9
+ from semantic_release.version.declarations.enum import VersionStampType
10
+ from semantic_release.version.declarations.i_version_replacer import IVersionReplacer
11
+
12
+ if TYPE_CHECKING: # pragma: no cover
13
+ from semantic_release.version.version import Version
14
+
15
+
16
+ class FileVersionDeclaration(IVersionReplacer):
17
+ """
18
+ IVersionReplacer implementation that replaces the entire file content
19
+ with the version string.
20
+
21
+ This is useful for files that contain only a version number, such as
22
+ VERSION files or similar single-line version storage files.
23
+ """
24
+
25
+ def __init__(self, path: Path | str, stamp_format: VersionStampType) -> None:
26
+ self._content: str | None = None
27
+ self._path = Path(path).resolve()
28
+ self._stamp_format = stamp_format
29
+
30
+ @property
31
+ def content(self) -> str:
32
+ """A cached property that stores the content of the configured source file."""
33
+ if self._content is None:
34
+ logger.debug("No content stored, reading from source file %s", self._path)
35
+
36
+ if not self._path.exists():
37
+ logger.debug(
38
+ f"path {self._path!r} does not exist, assuming empty content"
39
+ )
40
+ self._content = ""
41
+ else:
42
+ self._content = self._path.read_text()
43
+
44
+ return self._content
45
+
46
+ @content.deleter
47
+ def content(self) -> None:
48
+ self._content = None
49
+
50
+ @deprecated(
51
+ version="10.6.0",
52
+ reason="Function is unused and will be removed in a future release",
53
+ )
54
+ def parse(self) -> set[Version]:
55
+ raise NotImplementedError # pragma: no cover
56
+
57
+ def replace(self, new_version: Version) -> str:
58
+ """
59
+ Replace the file content with the new version string.
60
+
61
+ :param new_version: The new version number as a `Version` instance
62
+ :return: The new content (just the version string)
63
+ """
64
+ new_content = (
65
+ new_version.as_tag()
66
+ if self._stamp_format == VersionStampType.TAG_FORMAT
67
+ else str(new_version)
68
+ )
69
+
70
+ logger.debug(
71
+ "Replacing entire file content: path=%r old_content=%r new_content=%r",
72
+ self._path,
73
+ self.content.strip(),
74
+ new_content,
75
+ )
76
+
77
+ return new_content
78
+
79
+ def update_file_w_version(
80
+ self, new_version: Version, noop: bool = False
81
+ ) -> Path | None:
82
+ if noop:
83
+ if not self._path.exists():
84
+ logger.warning(
85
+ f"FILE NOT FOUND: file '{self._path}' does not exist but it will be created"
86
+ )
87
+
88
+ return self._path
89
+
90
+ new_content = self.replace(new_version)
91
+ if new_content == self.content.strip():
92
+ return None
93
+
94
+ self._path.write_text(f"{new_content}\n")
95
+ del self.content
96
+
97
+ return self._path
98
+
99
+ @classmethod
100
+ def from_string_definition(cls, replacement_def: str) -> FileVersionDeclaration:
101
+ """
102
+ Create an instance of self from a string representing one item
103
+ of the "version_variables" list in the configuration.
104
+
105
+ This method expects a definition in the format:
106
+ "file:*:format_type"
107
+
108
+ where:
109
+ - file is the path to the file
110
+ - * is the literal asterisk character indicating file replacement
111
+ - format_type is either "nf" (number format) or "tf" (tag format)
112
+ """
113
+ parts = replacement_def.split(":", maxsplit=2)
114
+
115
+ if len(parts) <= 1:
116
+ raise ValueError(
117
+ f"Invalid replacement definition {replacement_def!r}, missing ':'"
118
+ )
119
+
120
+ if len(parts) == 2:
121
+ # apply default version_type of "number_format" (ie. "1.2.3")
122
+ parts = [*parts, VersionStampType.NUMBER_FORMAT.value]
123
+
124
+ path, pattern, version_type = parts
125
+
126
+ # Validate that the pattern is exactly "*"
127
+ if pattern != "*":
128
+ raise ValueError(
129
+ f"Invalid pattern {pattern!r} for FileVersionDeclaration, expected '*'"
130
+ )
131
+
132
+ try:
133
+ stamp_type = VersionStampType(version_type)
134
+ except ValueError as err:
135
+ raise ValueError(
136
+ str.join(
137
+ " ",
138
+ [
139
+ "Invalid stamp type, must be one of:",
140
+ str.join(", ", [e.value for e in VersionStampType]),
141
+ ],
142
+ )
143
+ ) from err
144
+
145
+ return cls(path, stamp_type)
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  from abc import ABCMeta, abstractmethod
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ from deprecated.sphinx import deprecated
7
+
6
8
  if TYPE_CHECKING: # pragma: no cover
7
9
  from pathlib import Path
8
10
 
@@ -32,6 +34,10 @@ class IVersionReplacer(metaclass=ABCMeta):
32
34
  )
33
35
  )
34
36
 
37
+ @deprecated(
38
+ version="9.20.0",
39
+ reason="Function is unused and will be removed in a future release",
40
+ )
35
41
  @abstractmethod
36
42
  def parse(self) -> set[Version]:
37
43
  """