python-semantic-release 10.2.0__py3-none-any.whl → 10.5.2__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.
Files changed (24) hide show
  1. {python_semantic_release-10.2.0.dist-info → python_semantic_release-10.5.2.dist-info}/METADATA +14 -12
  2. {python_semantic_release-10.2.0.dist-info → python_semantic_release-10.5.2.dist-info}/RECORD +24 -20
  3. semantic_release/changelog/context.py +1 -1
  4. semantic_release/cli/commands/version.py +124 -39
  5. semantic_release/cli/config.py +8 -3
  6. semantic_release/cli/github_actions_output.py +96 -7
  7. semantic_release/cli/masking_filter.py +1 -3
  8. semantic_release/cli/util.py +2 -2
  9. semantic_release/commit_parser/__init__.py +23 -0
  10. semantic_release/commit_parser/conventional/__init__.py +17 -0
  11. semantic_release/commit_parser/conventional/options.py +72 -0
  12. semantic_release/commit_parser/conventional/options_monorepo.py +90 -0
  13. semantic_release/commit_parser/{conventional.py → conventional/parser.py} +85 -127
  14. semantic_release/commit_parser/conventional/parser_monorepo.py +467 -0
  15. semantic_release/errors.py +16 -0
  16. semantic_release/gitproject.py +187 -20
  17. semantic_release/version/algorithm.py +5 -18
  18. semantic_release/version/declarations/pattern.py +2 -2
  19. semantic_release/version/translator.py +23 -5
  20. semantic_release/version/version.py +9 -0
  21. {python_semantic_release-10.2.0.dist-info → python_semantic_release-10.5.2.dist-info}/WHEEL +0 -0
  22. {python_semantic_release-10.2.0.dist-info → python_semantic_release-10.5.2.dist-info}/entry_points.txt +0 -0
  23. {python_semantic_release-10.2.0.dist-info → python_semantic_release-10.5.2.dist-info}/licenses/LICENSE +0 -0
  24. {python_semantic_release-10.2.0.dist-info → python_semantic_release-10.5.2.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-semantic-release
3
- Version: 10.2.0
3
+ Version: 10.5.2
4
4
  Summary: Automatic Semantic Versioning for Python projects
5
- Author-email: Rolf Erik Lekang <me@rolflekang.com>
5
+ Author-email: Rolf Erik Lekang <me@rolflekang.com>, codejedi365 <codejedi365@gmail.com>
6
6
  License: MIT
7
7
  Project-URL: changelog, https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md
8
8
  Project-URL: documentation, https://python-semantic-release.readthedocs.io
@@ -17,7 +17,8 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
- Requires-Python: >=3.8
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Requires-Python: ~=3.8
21
22
  Description-Content-Type: text/x-rst
22
23
  License-File: LICENSE
23
24
  Requires-Dist: click~=8.1.0
@@ -26,7 +27,7 @@ Requires-Dist: gitpython~=3.0
26
27
  Requires-Dist: requests~=2.25
27
28
  Requires-Dist: jinja2~=3.1
28
29
  Requires-Dist: python-gitlab<7.0.0,>=4.0.0
29
- Requires-Dist: tomlkit~=0.11
30
+ Requires-Dist: tomlkit~=0.13.0
30
31
  Requires-Dist: dotty-dict~=1.3
31
32
  Requires-Dist: importlib-resources~=6.0
32
33
  Requires-Dist: pydantic~=2.0
@@ -35,11 +36,12 @@ Requires-Dist: shellingham~=1.5
35
36
  Requires-Dist: Deprecated~=1.2
36
37
  Provides-Extra: build
37
38
  Requires-Dist: build~=1.2; extra == "build"
39
+ Requires-Dist: tomlkit~=0.13.0; extra == "build"
38
40
  Provides-Extra: docs
39
- Requires-Dist: Sphinx~=6.0; extra == "docs"
40
- Requires-Dist: sphinxcontrib-apidoc==0.5.0; extra == "docs"
41
+ Requires-Dist: Sphinx~=7.4; extra == "docs"
42
+ Requires-Dist: sphinxcontrib-apidoc==0.6.0; extra == "docs"
41
43
  Requires-Dist: sphinx-autobuild==2024.2.4; extra == "docs"
42
- Requires-Dist: furo~=2024.1; extra == "docs"
44
+ Requires-Dist: furo~=2025.9; extra == "docs"
43
45
  Provides-Extra: test
44
46
  Requires-Dist: coverage[toml]~=7.0; extra == "test"
45
47
  Requires-Dist: filelock~=3.15; extra == "test"
@@ -48,9 +50,9 @@ Requires-Dist: freezegun~=1.5; extra == "test"
48
50
  Requires-Dist: pyyaml~=6.0; extra == "test"
49
51
  Requires-Dist: pytest~=8.3; extra == "test"
50
52
  Requires-Dist: pytest-clarity~=1.0; extra == "test"
51
- Requires-Dist: pytest-cov<7.0.0,>=5.0.0; extra == "test"
53
+ Requires-Dist: pytest-cov<8.0.0,>=5.0.0; extra == "test"
52
54
  Requires-Dist: pytest-env~=1.0; extra == "test"
53
- Requires-Dist: pytest-lazy-fixtures~=1.1.1; extra == "test"
55
+ Requires-Dist: pytest-lazy-fixtures~=1.4; extra == "test"
54
56
  Requires-Dist: pytest-mock~=3.0; extra == "test"
55
57
  Requires-Dist: pytest-order~=1.3; extra == "test"
56
58
  Requires-Dist: pytest-pretty~=1.2; extra == "test"
@@ -58,7 +60,7 @@ Requires-Dist: pytest-xdist~=3.0; extra == "test"
58
60
  Requires-Dist: responses~=0.25.0; extra == "test"
59
61
  Requires-Dist: requests-mock~=1.10; extra == "test"
60
62
  Provides-Extra: dev
61
- Requires-Dist: pre-commit~=3.5; extra == "dev"
63
+ Requires-Dist: pre-commit~=4.3; extra == "dev"
62
64
  Requires-Dist: tox~=4.11; extra == "dev"
63
65
  Requires-Dist: ruff==0.6.1; extra == "dev"
64
66
  Provides-Extra: mypy
@@ -88,5 +90,5 @@ The usage information and examples for this GitHub Action is available under
88
90
  the `GitHub Actions section`_ of `python-semantic-release.readthedocs.io`_.
89
91
 
90
92
  .. _python-semantic-release: https://pypi.org/project/python-semantic-release/
91
- .. _python-semantic-release.readthedocs.io: https://python-semantic-release.readthedocs.io/en/latest/
92
- .. _GitHub Actions section: https://python-semantic-release.readthedocs.io/en/latest/automatic-releases/github-actions.html
93
+ .. _python-semantic-release.readthedocs.io: https://python-semantic-release.readthedocs.io/en/stable/
94
+ .. _GitHub Actions section: https://python-semantic-release.readthedocs.io/en/stable/configuration/automatic-releases/github-actions.html
@@ -1,40 +1,44 @@
1
- python_semantic_release-10.2.0.dist-info/licenses/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
1
+ python_semantic_release-10.5.2.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
- semantic_release/errors.py,sha256=PY9rmviSFBZkqawW6VXbUfmF9C_RNOIObcmeGxLefMo,2904
7
- semantic_release/gitproject.py,sha256=UMvxcxqu9jRnZ3C5miNYYDh2i6hygLNEqo3-TuW4Pj4,9423
6
+ semantic_release/errors.py,sha256=FyocaqHbRhux-iNmCf9nI7awyUaGKjG9_5C_QDvhEas,3399
7
+ semantic_release/gitproject.py,sha256=qF4GVZh-aaS4bVV3OmFQLmXMAxHNFyZCwUaWPtUmZ1k,16546
8
8
  semantic_release/globals.py,sha256=IBhBbhZr2jx8dmpySnnu9m9jOGYu9Yu-vqHvAGQxgnw,464
9
9
  semantic_release/helpers.py,sha256=2uQXOiuiemiviVD52SH2FF6cn14p_gqvREeHvP7dexw,10012
10
10
  semantic_release/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  semantic_release/changelog/__init__.py,sha256=Bg6Xe5Vt32rWoMscW-hd4sUwiZqzWmsg4CD1EhMesMY,262
12
- semantic_release/changelog/context.py,sha256=WeLQ2BvYEWunIF8XEl6ldQE4IRg0d7r8mYSBi-TqK7o,5988
12
+ semantic_release/changelog/context.py,sha256=234LLcB_uuGaQ5N_zTbcQYF27ZwYIBmicIWkhFs7-aQ,5993
13
13
  semantic_release/changelog/release_history.py,sha256=kX6D9VReq85wnHz0D6JpmJJCfWA6axV6RToSM0O5laU,10602
14
14
  semantic_release/changelog/template.py,sha256=eqOjtVfBbksgTK4CAZOajfkaAcKueJ-FhFv99U3-J5E,5695
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=4UCx4-jzKpggb7dZM3fnZoi8rPiUGWtEmLiCfWlro7I,33458
18
+ semantic_release/cli/config.py,sha256=zNXlJHZBnFk2GEemgCg2Yw42EWihtFMot1twI7bEN-g,33668
19
19
  semantic_release/cli/const.py,sha256=h7XE2D0D__TAZSrUUtVszwvzpkHTMOiQCf97XQNbEvA,163
20
- semantic_release/cli/github_actions_output.py,sha256=5m0IwIUu-Ntse_Uwrxhn5lYmaVN16JUyOADvmEgsYxg,2293
21
- semantic_release/cli/masking_filter.py,sha256=GsTyaoZbUVJLXVMqeXhCttXK84UnBQ8cNDSHxd52sOc,3218
22
- semantic_release/cli/util.py,sha256=4hiqvMzuUzMJ0-vYn-mYoB5nctBHjL45EhLqApbc2DQ,3729
20
+ semantic_release/cli/github_actions_output.py,sha256=yC0nsMvEFGACjDwB8DdmGKwNGI8aIIhDxRHrmcS7tzA,5410
21
+ semantic_release/cli/masking_filter.py,sha256=7t7XFL7Iy4QTvaYevZ-jnTkJBz15GoBw1vjW3hihe38,3159
22
+ semantic_release/cli/util.py,sha256=4rf4xDHb7l-XpdcFNtslFz1xHslnBu6cTgWHXVnQXQs,3746
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
25
  semantic_release/cli/commands/generate_config.py,sha256=2xZOu3NpyhBp0pWr7d8ugKl_kjqQgpSsSMHq5wHTfrE,1699
26
26
  semantic_release/cli/commands/main.py,sha256=u1zhkkvKCZ2TtUqjzvdFTe5UZsvfws_pjqqo6CY0bBo,4351
27
27
  semantic_release/cli/commands/publish.py,sha256=CE_LJTxFnc337MfpsfdJopi7QCwwE13GqGNQ-dNgWis,2871
28
- semantic_release/cli/commands/version.py,sha256=d72k2YVkynbBcjFPbWJr80PjjYE7JMd_btaL4baByaM,25762
29
- semantic_release/commit_parser/__init__.py,sha256=6euiDgj9bwOx1rP96vUjq090usviXkbo7OVOnRBGfcw,742
28
+ semantic_release/cli/commands/version.py,sha256=1zJ96_WSQh4bY_75P3eTJTfwrEZcJGHf7XJO2yA66SA,29459
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/conventional.py,sha256=Iyy8txkshK8HikizFVa7pnJHBSWDhOhF45zR866QxQM,18672
33
32
  semantic_release/commit_parser/emoji.py,sha256=0VecUMMcj43UaKDZ2GaFFUG26zOJAvXsEobTHaXNkk0,17749
34
33
  semantic_release/commit_parser/scipy.py,sha256=gA9TfmrxGVvtv6kki6N_9abJx-_WlpBsSX2TBh0YgPw,19463
35
34
  semantic_release/commit_parser/tag.py,sha256=bVO2XghM0G_eW2rG9Xc2q5TPsjtxr-xcHK5RpE1u_HM,3537
36
35
  semantic_release/commit_parser/token.py,sha256=1_q8mJ4SRu7kNfa-Nxr8fEyuvCfjPgiPEitqSP1KR5g,7904
37
36
  semantic_release/commit_parser/util.py,sha256=hcLjc16o7l6y_5Hfl8IxmF4ijaJD62JdjdB2DJWAe-g,3959
37
+ semantic_release/commit_parser/conventional/__init__.py,sha256=JHPL3S6trM4Le9MXnXc6iPYxxQnE4j0LG83_BWIomOw,602
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
40
+ semantic_release/commit_parser/conventional/parser.py,sha256=Pex26cvMqAKx9NPH3KROYg7k76R0Nsbs4aHor5SbMZg,16671
41
+ semantic_release/commit_parser/conventional/parser_monorepo.py,sha256=nVAc_gK4tJ5UM_rd9KbXgBWeBgrI_q2-vav3_4748r8,21189
38
42
  semantic_release/data/templates/conventional/md/.release_notes.md.j2,sha256=DlMVAJMGqE27TwJ-2kviYaFhd3uWqXiU6Ikl15Ukne8,2512
39
43
  semantic_release/data/templates/conventional/md/CHANGELOG.md.j2,sha256=FZmrQ-qOIoSoJmAa_NFaRelfmqUpypU2xlDeScdGOf4,729
40
44
  semantic_release/data/templates/conventional/md/.components/changelog_header.md.j2,sha256=qNxTuSr59CV_yyimVU_RYp5azCnK0l6nJ03Zf0u5Ugg,166
@@ -64,17 +68,17 @@ semantic_release/hvcs/remote_hvcs_base.py,sha256=QtkjdMy9l-c7UOtyPz25cqVOkCk4IU-
64
68
  semantic_release/hvcs/token_auth.py,sha256=ZjT56-NIPB4OKIt1qwHCu1TavXnrWFIBl9ARlg56hgU,663
65
69
  semantic_release/hvcs/util.py,sha256=PUNV4yUlpzDtNCFmh2joaPdU4JyfUBnVp0zaQsT9EDQ,2871
66
70
  semantic_release/version/__init__.py,sha256=CLhtGQry9dLIij5XyRa9ZevxU_1p8tjMTSQ-K_GMpWM,270
67
- semantic_release/version/algorithm.py,sha256=84N2pisD-W9z2WuCNqwq1WHteGlnM4ECKt1maAXP8N8,16687
71
+ semantic_release/version/algorithm.py,sha256=IxgYNF78W7qdMzdV4WsoljwfJgB-sn2XdfkevD0aZlo,16254
68
72
  semantic_release/version/declaration.py,sha256=eot_lUyFaEhzK4bPncfv9tahf51LdxZP6EaS54h3aAs,3635
69
- semantic_release/version/translator.py,sha256=LjmsMHWJJOG8ES6lRhjQsP2t8pmw4Ux1XVYzvROMR_M,3047
70
- semantic_release/version/version.py,sha256=3QlPKsrmNnFH71GlsYvpI-WwhpkJEs_JrDBbMaepwJY,14183
73
+ semantic_release/version/translator.py,sha256=iIfu3WreB9qqPHgqJLILbBluVQQNcpP0DEsnn_WzAaM,3689
74
+ semantic_release/version/version.py,sha256=Y3Qqqv7CypirikT7jNqqFMNAvR2UjV-VQx-c2G0mZc0,14519
71
75
  semantic_release/version/declarations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
76
  semantic_release/version/declarations/enum.py,sha256=3n5Py9DoFkmItIdsmtQrJgmAhepTv_1EnogzSdXu1Wg,244
73
77
  semantic_release/version/declarations/i_version_replacer.py,sha256=oP6BxJuxwI44roI6448tomShv1sMoy9ry8TlhhIQtfc,2416
74
- semantic_release/version/declarations/pattern.py,sha256=MpUmsHYGAVAuFSKSb29FLcWeUCEHG_TRyhMO-2DWAAs,8308
78
+ semantic_release/version/declarations/pattern.py,sha256=sKk0uQpJjWVZc8RJUjxQoEPUvFLxXNGGBow5h1IqCTM,8378
75
79
  semantic_release/version/declarations/toml.py,sha256=2K4DtX5Qq1iHT8cG8mISPTMmp50w6Av0KmLAKZPYqq8,4931
76
- python_semantic_release-10.2.0.dist-info/METADATA,sha256=_wRj0vN8sxY4OE7ls1MbS9kWhFLnT1gD2AWwTjp-D-A,3913
77
- python_semantic_release-10.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
78
- python_semantic_release-10.2.0.dist-info/entry_points.txt,sha256=kzkCyDJsMOwgpFwEWKE9wxN1tXaUP6g6GIO4xtc0QuE,162
79
- python_semantic_release-10.2.0.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
80
- python_semantic_release-10.2.0.dist-info/RECORD,,
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,,
@@ -119,7 +119,7 @@ def read_file(filepath: str) -> str:
119
119
  return rfd.read()
120
120
 
121
121
  except FileNotFoundError as err:
122
- logging.warning(err)
122
+ logging.warning(str(err))
123
123
  return ""
124
124
 
125
125
 
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING
10
10
  import click
11
11
  import shellingham # type: ignore[import]
12
12
  from click_option_group import MutuallyExclusiveOptionGroup, optgroup
13
- from git import Repo
13
+ from git import GitCommandError, Repo
14
14
  from requests import HTTPError
15
15
 
16
16
  from semantic_release.changelog.release_history import ReleaseHistory
@@ -18,18 +18,27 @@ from semantic_release.cli.changelog_writer import (
18
18
  generate_release_notes,
19
19
  write_changelog_files,
20
20
  )
21
- from semantic_release.cli.github_actions_output import VersionGitHubActionsOutput
21
+ from semantic_release.cli.github_actions_output import (
22
+ PersistenceMode,
23
+ VersionGitHubActionsOutput,
24
+ )
22
25
  from semantic_release.cli.util import noop_report, rprint
23
26
  from semantic_release.const import DEFAULT_SHELL, DEFAULT_VERSION
24
27
  from semantic_release.enums import LevelBump
25
28
  from semantic_release.errors import (
26
29
  BuildDistributionsError,
30
+ DetachedHeadGitError,
27
31
  GitCommitEmptyIndexError,
32
+ GitFetchError,
28
33
  InternalError,
34
+ LocalGitError,
29
35
  UnexpectedResponse,
36
+ UnknownUpstreamBranchError,
37
+ UpstreamBranchChangedError,
30
38
  )
31
39
  from semantic_release.gitproject import GitProject
32
40
  from semantic_release.globals import logger
41
+ from semantic_release.hvcs.github import Github
33
42
  from semantic_release.hvcs.remote_hvcs_base import RemoteHvcsBase
34
43
  from semantic_release.version.algorithm import (
35
44
  next_version,
@@ -71,10 +80,13 @@ def is_forced_prerelease(
71
80
  )
72
81
 
73
82
 
74
- def last_released(repo_dir: Path, tag_format: str) -> tuple[Tag, Version] | None:
83
+ def last_released(
84
+ repo_dir: Path, tag_format: str, add_partial_tags: bool = False
85
+ ) -> tuple[Tag, Version] | None:
75
86
  with Repo(str(repo_dir)) as git_repo:
76
87
  ts_and_vs = tags_and_versions(
77
- git_repo.tags, VersionTranslator(tag_format=tag_format)
88
+ git_repo.tags,
89
+ VersionTranslator(tag_format=tag_format, add_partial_tags=add_partial_tags),
78
90
  )
79
91
 
80
92
  return ts_and_vs[0] if ts_and_vs else None
@@ -445,7 +457,11 @@ def version( # noqa: C901
445
457
  if print_last_released or print_last_released_tag:
446
458
  # TODO: get tag format a better way
447
459
  if not (
448
- last_release := last_released(config.repo_dir, tag_format=config.tag_format)
460
+ last_release := last_released(
461
+ config.repo_dir,
462
+ tag_format=config.tag_format,
463
+ add_partial_tags=config.add_partial_tags,
464
+ )
449
465
  ):
450
466
  logger.warning("No release tags found.")
451
467
  return
@@ -466,7 +482,16 @@ def version( # noqa: C901
466
482
  major_on_zero = runtime.major_on_zero
467
483
  no_verify = runtime.no_git_verify
468
484
  opts = runtime.global_cli_options
469
- gha_output = VersionGitHubActionsOutput(released=False)
485
+ add_partial_tags = config.add_partial_tags
486
+ gha_output = VersionGitHubActionsOutput(
487
+ gh_client=hvcs_client if isinstance(hvcs_client, Github) else None,
488
+ mode=(
489
+ PersistenceMode.TEMPORARY
490
+ if opts.noop or (not commit_changes and not create_tag)
491
+ else PersistenceMode.PERMANENT
492
+ ),
493
+ released=False,
494
+ )
470
495
 
471
496
  forced_level_bump = None if not force_level else LevelBump.from_string(force_level)
472
497
  prerelease = is_forced_prerelease(
@@ -479,6 +504,17 @@ def version( # noqa: C901
479
504
  logger.info("Forcing use of %s as the prerelease token", prerelease_token)
480
505
  translator.prerelease_token = prerelease_token
481
506
 
507
+ # Check if the repository is shallow and unshallow it if necessary
508
+ # This ensures we have the full history for commit analysis
509
+ project = GitProject(
510
+ directory=runtime.repo_dir,
511
+ commit_author=runtime.commit_author,
512
+ credential_masker=runtime.masker,
513
+ )
514
+ if project.is_shallow_clone():
515
+ logger.info("Repository is a shallow clone, converting to full clone...")
516
+ project.git_unshallow(noop=opts.noop)
517
+
482
518
  # Only push if we're committing changes
483
519
  if push_changes and not commit_changes and not create_tag:
484
520
  logger.info("changes will not be pushed because --no-commit disables pushing")
@@ -537,7 +573,8 @@ def version( # noqa: C901
537
573
 
538
574
  # Update GitHub Actions output value with new version & set delayed write
539
575
  gha_output.version = new_version
540
- ctx.call_on_close(gha_output.write_if_possible)
576
+ if isinstance(hvcs_client, Github):
577
+ ctx.call_on_close(gha_output.write_if_possible)
541
578
 
542
579
  # Make string variant of version or appropriate tag as necessary
543
580
  version_to_print = str(new_version) if not print_only_tag else new_version.as_tag()
@@ -572,6 +609,12 @@ def version( # noqa: C901
572
609
  if print_only or print_only_tag:
573
610
  return
574
611
 
612
+ # TODO: need a better way as this is inconsistent if releasing older version patches
613
+ if last_release := last_released(config.repo_dir, tag_format=config.tag_format):
614
+ # If we have a last release, we can set the previous version for the
615
+ # GitHub Actions output
616
+ gha_output.prev_version = last_release[1]
617
+
575
618
  with Repo(str(runtime.repo_dir)) as git_repo:
576
619
  release_history = ReleaseHistory.from_git_history(
577
620
  repo=git_repo,
@@ -641,10 +684,27 @@ def version( # noqa: C901
641
684
  click.echo("Build failed, aborting release", err=True)
642
685
  ctx.exit(1)
643
686
 
644
- project = GitProject(
645
- directory=runtime.repo_dir,
646
- commit_author=runtime.commit_author,
647
- credential_masker=runtime.masker,
687
+ license_cfg = runtime.project_metadata.get(
688
+ "license-expression",
689
+ runtime.project_metadata.get(
690
+ "license",
691
+ "",
692
+ ),
693
+ )
694
+
695
+ license_cfg = "" if not isinstance(license_cfg, (str, dict)) else license_cfg
696
+ license_cfg = (
697
+ license_cfg.get("text", "") if isinstance(license_cfg, dict) else license_cfg
698
+ )
699
+
700
+ gha_output.release_notes = release_notes = generate_release_notes(
701
+ hvcs_client,
702
+ release=release_history.released[new_version],
703
+ template_dir=runtime.template_dir,
704
+ history=release_history,
705
+ style=runtime.changelog_style,
706
+ mask_initial_release=runtime.changelog_mask_initial_release,
707
+ license_name="" if not isinstance(license_cfg, str) else license_cfg,
648
708
  )
649
709
 
650
710
  # Preparing for committing changes; we always stage files even if we're not committing them in order to support a two-stage commit
@@ -662,13 +722,14 @@ def version( # noqa: C901
662
722
  )
663
723
  except GitCommitEmptyIndexError:
664
724
  logger.info("No local changes to add to any commit, skipping")
725
+ commit_changes = False
665
726
 
666
727
  # Tag the version after potentially creating a new HEAD commit.
667
728
  # This way if no source code is modified, i.e. all metadata updates
668
729
  # are disabled, and the changelog generation is disabled or it's not
669
730
  # modified, then the HEAD commit will be tagged as a release commit
670
731
  # despite not being made by PSR
671
- if commit_changes or create_tag:
732
+ if create_tag:
672
733
  project.git_tag(
673
734
  tag_name=new_version.as_tag(),
674
735
  message=new_version.as_tag(),
@@ -676,12 +737,42 @@ def version( # noqa: C901
676
737
  noop=opts.noop,
677
738
  )
678
739
 
740
+ with Repo(str(runtime.repo_dir)) as git_repo:
741
+ gha_output.commit_sha = git_repo.head.commit.hexsha
742
+
679
743
  if push_changes:
680
744
  remote_url = runtime.hvcs_client.remote_url(
681
745
  use_token=not runtime.ignore_token_for_push
682
746
  )
683
747
 
684
748
  if commit_changes:
749
+ # Verify that the upstream branch has not changed before pushing
750
+ # This prevents conflicts if another commit was pushed while we were preparing the release
751
+ # We check HEAD~1 because we just made a release commit
752
+ try:
753
+ project.verify_upstream_unchanged(
754
+ local_ref="HEAD~1",
755
+ upstream_ref=config.remote.name,
756
+ noop=opts.noop,
757
+ )
758
+ except UpstreamBranchChangedError as exc:
759
+ click.echo(str(exc), err=True)
760
+ click.echo(
761
+ "Upstream branch has changed. Please pull the latest changes and try again.",
762
+ err=True,
763
+ )
764
+ ctx.exit(1)
765
+ except (
766
+ DetachedHeadGitError,
767
+ GitCommandError,
768
+ UnknownUpstreamBranchError,
769
+ GitFetchError,
770
+ LocalGitError,
771
+ ) as exc:
772
+ click.echo(str(exc), err=True)
773
+ click.echo("Unable to verify upstream due to error!", err=True)
774
+ ctx.exit(1)
775
+
685
776
  # TODO: integrate into push branch
686
777
  with Repo(str(runtime.repo_dir)) as git_repo:
687
778
  active_branch = git_repo.active_branch.name
@@ -699,6 +790,27 @@ def version( # noqa: C901
699
790
  tag=new_version.as_tag(),
700
791
  noop=opts.noop,
701
792
  )
793
+ # Create or update partial tags for releases
794
+ if add_partial_tags and not prerelease:
795
+ partial_tags = [new_version.as_major_tag(), new_version.as_minor_tag()]
796
+ # If build metadata is set, also retag the version without the metadata
797
+ if build_metadata:
798
+ partial_tags.append(new_version.as_patch_tag())
799
+
800
+ for partial_tag in partial_tags:
801
+ project.git_tag(
802
+ tag_name=partial_tag,
803
+ message=f"{partial_tag} => {new_version.as_tag()}",
804
+ isotimestamp=commit_date.isoformat(),
805
+ noop=opts.noop,
806
+ force=True,
807
+ )
808
+ project.git_push_tag(
809
+ remote_url=remote_url,
810
+ tag=partial_tag,
811
+ noop=opts.noop,
812
+ force=True,
813
+ )
702
814
 
703
815
  # Update GitHub Actions output value now that release has occurred
704
816
  gha_output.released = True
@@ -710,33 +822,6 @@ def version( # noqa: C901
710
822
  logger.info("Remote does not support releases. Skipping release creation...")
711
823
  return
712
824
 
713
- license_cfg = runtime.project_metadata.get(
714
- "license-expression",
715
- runtime.project_metadata.get(
716
- "license",
717
- "",
718
- ),
719
- )
720
-
721
- if not isinstance(license_cfg, (str, dict)) or license_cfg is None:
722
- license_cfg = ""
723
-
724
- license_name = (
725
- license_cfg.get("text", "")
726
- if isinstance(license_cfg, dict)
727
- else license_cfg or ""
728
- )
729
-
730
- release_notes = generate_release_notes(
731
- hvcs_client,
732
- release=release_history.released[new_version],
733
- template_dir=runtime.template_dir,
734
- history=release_history,
735
- style=runtime.changelog_style,
736
- mask_initial_release=runtime.changelog_mask_initial_release,
737
- license_name=license_name,
738
- )
739
-
740
825
  exception: Exception | None = None
741
826
  help_message = ""
742
827
  try:
@@ -39,6 +39,7 @@ from semantic_release.cli.masking_filter import MaskingFilter
39
39
  from semantic_release.commit_parser import (
40
40
  AngularCommitParser,
41
41
  CommitParser,
42
+ ConventionalCommitMonorepoParser,
42
43
  ConventionalCommitParser,
43
44
  EmojiCommitParser,
44
45
  ParseResult,
@@ -71,9 +72,10 @@ class HvcsClient(str, Enum):
71
72
  GITEA = "gitea"
72
73
 
73
74
 
74
- _known_commit_parsers: Dict[str, type[CommitParser]] = {
75
- "conventional": ConventionalCommitParser,
75
+ _known_commit_parsers: dict[str, type[CommitParser[Any, Any]]] = {
76
76
  "angular": AngularCommitParser,
77
+ "conventional": ConventionalCommitParser,
78
+ "conventional-monorepo": ConventionalCommitMonorepoParser,
77
79
  "emoji": EmojiCommitParser,
78
80
  "scipy": ScipyCommitParser,
79
81
  "tag": TagCommitParser,
@@ -364,6 +366,7 @@ class RawConfig(BaseModel):
364
366
  remote: RemoteConfig = RemoteConfig()
365
367
  no_git_verify: bool = False
366
368
  tag_format: str = "v{version}"
369
+ add_partial_tags: bool = False
367
370
  publish: PublishConfig = PublishConfig()
368
371
  version_toml: Optional[Tuple[str, ...]] = None
369
372
  version_variables: Optional[Tuple[str, ...]] = None
@@ -825,7 +828,9 @@ class RuntimeContext:
825
828
 
826
829
  # version_translator
827
830
  version_translator = VersionTranslator(
828
- tag_format=raw.tag_format, prerelease_token=branch_config.prerelease_token
831
+ tag_format=raw.tag_format,
832
+ prerelease_token=branch_config.prerelease_token,
833
+ add_partial_tags=raw.add_partial_tags,
829
834
  )
830
835
 
831
836
  build_cmd_env = {}
@@ -1,21 +1,44 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ from enum import Enum
5
+ from re import compile as regexp
6
+ from typing import TYPE_CHECKING
4
7
 
5
8
  from semantic_release.globals import logger
6
9
  from semantic_release.version.version import Version
7
10
 
11
+ if TYPE_CHECKING:
12
+ from typing import Any
13
+
14
+ from semantic_release.hvcs.github import Github
15
+
16
+
17
+ class PersistenceMode(Enum):
18
+ TEMPORARY = "temporary"
19
+ PERMANENT = "permanent"
20
+
8
21
 
9
22
  class VersionGitHubActionsOutput:
10
23
  OUTPUT_ENV_VAR = "GITHUB_OUTPUT"
11
24
 
12
25
  def __init__(
13
26
  self,
27
+ gh_client: Github | None = None,
28
+ mode: PersistenceMode = PersistenceMode.PERMANENT,
14
29
  released: bool | None = None,
15
30
  version: Version | None = None,
31
+ commit_sha: str | None = None,
32
+ release_notes: str | None = None,
33
+ prev_version: Version | None = None,
16
34
  ) -> None:
35
+ self._gh_client = gh_client
36
+ self._mode = mode
17
37
  self._released = released
18
38
  self._version = version
39
+ self._commit_sha = commit_sha
40
+ self._release_notes = release_notes
41
+ self._prev_version = prev_version
19
42
 
20
43
  @property
21
44
  def released(self) -> bool | None:
@@ -23,7 +46,7 @@ class VersionGitHubActionsOutput:
23
46
 
24
47
  @released.setter
25
48
  def released(self, value: bool) -> None:
26
- if type(value) is not bool:
49
+ if not isinstance(value, bool):
27
50
  raise TypeError("output 'released' is boolean")
28
51
  self._released = value
29
52
 
@@ -33,7 +56,7 @@ class VersionGitHubActionsOutput:
33
56
 
34
57
  @version.setter
35
58
  def version(self, value: Version) -> None:
36
- if type(value) is not Version:
59
+ if not isinstance(value, Version):
37
60
  raise TypeError("output 'released' should be a Version")
38
61
  self._version = value
39
62
 
@@ -45,26 +68,92 @@ class VersionGitHubActionsOutput:
45
68
  def is_prerelease(self) -> bool | None:
46
69
  return self.version.is_prerelease if self.version is not None else None
47
70
 
71
+ @property
72
+ def commit_sha(self) -> str | None:
73
+ return self._commit_sha if self._commit_sha else None
74
+
75
+ @commit_sha.setter
76
+ def commit_sha(self, value: str) -> None:
77
+ if not isinstance(value, str):
78
+ raise TypeError("output 'commit_sha' should be a string")
79
+
80
+ if not regexp(r"^[0-9a-f]{40}$").match(value):
81
+ raise ValueError(
82
+ "output 'commit_sha' should be a valid 40-hex-character SHA"
83
+ )
84
+
85
+ self._commit_sha = value
86
+
87
+ @property
88
+ def release_notes(self) -> str | None:
89
+ return self._release_notes if self._release_notes else None
90
+
91
+ @release_notes.setter
92
+ def release_notes(self, value: str) -> None:
93
+ if not isinstance(value, str):
94
+ raise TypeError("output 'release_notes' should be a string")
95
+ self._release_notes = value
96
+
97
+ @property
98
+ def prev_version(self) -> Version | None:
99
+ if not self.released:
100
+ return self.version
101
+ return self._prev_version if self._prev_version else None
102
+
103
+ @prev_version.setter
104
+ def prev_version(self, value: Version) -> None:
105
+ if not isinstance(value, Version):
106
+ raise TypeError("output 'prev_version' should be a Version")
107
+ self._prev_version = value
108
+
109
+ @property
110
+ def gh_client(self) -> Github:
111
+ if not self._gh_client:
112
+ raise ValueError("GitHub client not set, cannot create links")
113
+ return self._gh_client
114
+
48
115
  def to_output_text(self) -> str:
49
- missing = set()
116
+ missing: set[str] = set()
50
117
  if self.version is None:
51
118
  missing.add("version")
52
119
  if self.released is None:
53
120
  missing.add("released")
121
+ if self.released:
122
+ if self.release_notes is None:
123
+ missing.add("release_notes")
124
+ if self._mode is PersistenceMode.PERMANENT and self.commit_sha is None:
125
+ missing.add("commit_sha")
54
126
 
55
127
  if missing:
56
128
  raise ValueError(
57
129
  f"some required outputs were not set: {', '.join(missing)}"
58
130
  )
59
131
 
60
- outputs = {
132
+ output_values: dict[str, Any] = {
61
133
  "released": str(self.released).lower(),
62
134
  "version": str(self.version),
63
135
  "tag": self.tag,
64
136
  "is_prerelease": str(self.is_prerelease).lower(),
137
+ "link": self.gh_client.create_release_url(self.tag) if self.tag else "",
138
+ "previous_version": str(self.prev_version) if self.prev_version else "",
139
+ "commit_sha": self.commit_sha if self.commit_sha else "",
65
140
  }
66
141
 
67
- return str.join("", [f"{key}={value!s}\n" for key, value in outputs.items()])
142
+ multiline_output_values: dict[str, str] = {
143
+ "release_notes": self.release_notes if self.release_notes else "",
144
+ }
145
+
146
+ output_lines = [
147
+ *[f"{key}={value!s}{os.linesep}" for key, value in output_values.items()],
148
+ *[
149
+ f"{key}<<EOF{os.linesep}{value}EOF{os.linesep}"
150
+ if value
151
+ else f"{key}={os.linesep}"
152
+ for key, value in multiline_output_values.items()
153
+ ],
154
+ ]
155
+
156
+ return str.join("", output_lines)
68
157
 
69
158
  def write_if_possible(self, filename: str | None = None) -> None:
70
159
  output_file = filename or os.getenv(self.OUTPUT_ENV_VAR)
@@ -72,5 +161,5 @@ class VersionGitHubActionsOutput:
72
161
  logger.info("not writing GitHub Actions output, as no file specified")
73
162
  return
74
163
 
75
- with open(output_file, "a", encoding="utf-8") as f:
76
- f.write(self.to_output_text())
164
+ with open(output_file, "ab") as f:
165
+ f.write(self.to_output_text().encode("utf-8"))
@@ -62,9 +62,7 @@ class MaskingFilter(LoggingFilter):
62
62
 
63
63
  def mask(self, msg: str) -> str:
64
64
  if not isinstance(msg, str):
65
- logger.debug( # type: ignore[unreachable]
66
- "cannot mask object of type %s", type(msg)
67
- )
65
+ logger.debug("cannot mask object of type %s", type(msg))
68
66
  return msg
69
67
  for mask, values in self._redact_patterns.items():
70
68
  repl_string = (