Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,28 @@ jobs:
python -c 'import pathlib, semantic_release; print(f"PKG_INSTALLED_DIR={pathlib.Path(semantic_release.__file__).resolve().parent}")' | Tee-Object -Variable cmdOutput
echo $cmdOutput >> $env:GITHUB_OUTPUT

- name: Setup | Harden Windows runner for git-heavy tests
if: runner.os == 'Windows'
shell: pwsh
run: |
# 1. Exclude workspace & temp dirs from Defender real-time scanning
Add-MpPreference -ExclusionPath "${{ github.workspace }}"
Add-MpPreference -ExclusionPath "$env:TEMP"
Add-MpPreference -ExclusionProcess "git.exe"
Add-MpPreference -ExclusionProcess "python.exe"

# 2. Move temp (pytest tmp_path factory) to the faster D: drive
New-Item -ItemType Directory -Force -Path "D:\tmp" | Out-Null
"TEMP=D:\tmp" >> $env:GITHUB_ENV
"TMP=D:\tmp" >> $env:GITHUB_ENV

# 3. Disable git background features that race with rapid repo churn
git config --global core.fsmonitor false
git config --global core.untrackedCache false
git config --global gc.auto 0
git config --global maintenance.auto false
git config --global core.longpaths true

- name: Test | Run pytest -m e2e
id: tests
shell: pwsh
Expand All @@ -349,13 +371,13 @@ jobs:
# Required for GitPython to work on Windows because of getpass.getuser()
# USERNAME: "runneradmin"
# COLUMNS: 150
# Because GHA is currently broken on Windows to pass these varables, we do it manually
# Because GHA is currently broken on Windows to pass these variables, we do it manually
run: |
$env:USERNAME = "runneradmin"
$env:COLUMNS = 150
pytest `
-vv `
-nauto `
-n 2 `
-m e2e `
`--cov=${{ steps.install.outputs.PKG_INSTALLED_DIR }} `
`--cov-context=test `
Expand All @@ -377,7 +399,7 @@ jobs:
if: ${{ failure() && steps.tests.outcome == 'failure' }}
with:
name: ${{ format('tested-repos-{0}-{1}', matrix.os, matrix.python-version) }}
path: ~/AppData/Local/Temp/pytest-of-runneradmin/pytest-current/*
path: D:\tmp\pytest-of-runneradmin\pytest-current\*
include-hidden-files: true
if-no-files-found: error
retention-days: 1
Expand Down
32 changes: 32 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,38 @@
CHANGELOG
=========

.. _changelog-v10.6.2:

v10.6.2 (2026-08-28)
====================

馃 Bug Fixes
------------

* **cmd-changelog**: Fix reading of TOML when file was created on Windows (`9a56ace`_)

* **config**: Validate commit author in config internally, closes `#1476`_ (`PR#1477`_, `10afa8e`_)

鈿欙笍 Build System
----------------

* **deps**: Adjust ``importlib`` use to maintain 3.8 support (`59fea4f`_)

* **deps**: Bump ``rich`` requirement from ``v14.0+`` to ``v15.0+`` (`PR#1464`_, `abd942d`_)

* **deps**: Bump ``tomlkit`` requirement to ``~=0.15.0`` (`PR#1463`_, `429588d`_)

.. _#1476: https://github.com/python-semantic-release/python-semantic-release/issues/1476
.. _10afa8e: https://github.com/python-semantic-release/python-semantic-release/commit/10afa8ec4be54216258b275b5e1be0facf8ea8d7
.. _429588d: https://github.com/python-semantic-release/python-semantic-release/commit/429588da71dfc7bd88ff27618cec2fe843dedd7c
.. _59fea4f: https://github.com/python-semantic-release/python-semantic-release/commit/59fea4f231bd84c5af58785d9fd487cbcf8d3407
.. _9a56ace: https://github.com/python-semantic-release/python-semantic-release/commit/9a56ace313a1f3710cc0e0683285cde133704f01
.. _abd942d: https://github.com/python-semantic-release/python-semantic-release/commit/abd942d71c077e012eb49c43e094c9bf4a352a44
.. _PR#1463: https://github.com/python-semantic-release/python-semantic-release/pull/1463
.. _PR#1464: https://github.com/python-semantic-release/python-semantic-release/pull/1464
.. _PR#1477: https://github.com/python-semantic-release/python-semantic-release/pull/1477


.. _changelog-v10.6.1:

v10.6.1 (2026-07-06)
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration/automatic-releases/github-actions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -893,14 +893,14 @@ to the GitHub Release Assets as well.
- name: Action | Semantic Version Release
id: release
# Adjust tag with desired version if applicable.
uses: python-semantic-release/python-semantic-release@COMMIT_HASH # v10.6.1
uses: python-semantic-release/python-semantic-release@COMMIT_HASH # v10.6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
git_committer_name: "github-actions"
git_committer_email: "actions@users.noreply.github.com"

- name: Publish | Upload to GitHub Release Assets
uses: python-semantic-release/publish-action@COMMIT_HASH # v10.6.1
uses: python-semantic-release/publish-action@COMMIT_HASH # v10.6.2
if: steps.release.outputs.released == 'true'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "python-semantic-release"
version = "10.6.1"
version = "10.6.2"
description = "Automatic Semantic Versioning for Python projects"
requires-python = "~= 3.8"
license = { text = "MIT" }
Expand Down
2 changes: 1 addition & 1 deletion src/gh_action/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
python-semantic-release == 10.6.1
python-semantic-release == 10.6.2
7 changes: 6 additions & 1 deletion src/semantic_release/cli/commands/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ def get_license_name_for_release(tag_name: str, project_root: Path) -> str:
toml_contents = git_repo.git.show(
f"{tag_name}:{proj_toml.relative_to(project_root)}"
)
config_toml = tomlkit.parse(toml_contents)
# Normalize line endings: `git show` returns the blob verbatim, which
# may contain CRLF (or bare CR) when committed from Windows.
# tomlkit >= 0.15 rejects bare carriage returns as invalid characters.
config_toml = tomlkit.parse(
toml_contents.replace("\r\n", "\n").replace("\r", "")
)
project_metadata = config_toml.unwrap().get("project", project_metadata)
break

Expand Down
87 changes: 74 additions & 13 deletions src/semantic_release/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
Field,
RootModel,
ValidationError,
field_serializer,
field_validator,
model_validator,
)
Expand Down Expand Up @@ -364,8 +365,12 @@ class RawConfig(BaseModel):
build_command: Optional[str] = None
build_command_env: List[str] = []
changelog: ChangelogConfig = ChangelogConfig()
commit_author: MaybeFromEnv = EnvConfigVar(
env="GIT_COMMIT_AUTHOR", default=DEFAULT_COMMIT_AUTHOR
commit_author: Actor = Field(
default=cast(
"Actor",
EnvConfigVar(env="GIT_COMMIT_AUTHOR", default=DEFAULT_COMMIT_AUTHOR),
),
validate_default=True,
)
commit_message: str = COMMIT_MESSAGE
commit_parser: NonEmptyString = "conventional"
Expand All @@ -390,6 +395,72 @@ def convert_str_to_path(cls, value: Any) -> Path:
raise TypeError(f"Invalid type: {type(value)}, expected str or Path.")
return Path(value)

# Note: mode="plain" must be declared before mode="before" here, as pydantic
# composes same-field validators in declaration order and a later "before"
# validator wraps (runs prior to) an earlier "plain" validator.
@field_validator("commit_author", mode="plain")
@classmethod
def validate_commit_author(cls, val: Any) -> Actor:
if isinstance(val, Actor):
return val

if isinstance(val, dict):
if "name" not in val or "email" not in val:
msg = "commit_author dict must contain 'name' and 'email' keys."
raise ValueError(msg)
if not isinstance(val["name"], str) or not isinstance(val["email"], str):
msg = "commit_author 'name' and 'email' must be strings."
raise ValueError(msg) # noqa: TRY004
if not val["name"].strip() or not val["email"].strip():
msg = "commit_author 'name' and 'email' cannot be empty."
raise ValueError(msg)
# TODO: add email format validation (breaking change)
return Actor(**val)

if isinstance(val, str):
if not val.strip():
msg = "commit_author string cannot be empty."
raise ValueError(msg)

name_email_pattern = regexp(
r"^(?P<name>[^<]{1,255}) ?<(?P<email>[^>]{1,320})>$"
)
value = val.strip().splitlines()[0]

if not (m := name_email_pattern.search(value)):
msg = "commit_author string must be in the format 'Name <email>'."
raise ValueError(msg)

# TODO: add email format validation (breaking change)
email = m.group("email").strip()

return Actor(name=m.group("name").strip(), email=email)

msg = f"Invalid type for commit_author: {type(val)}, expected Actor, dict, or str."
raise TypeError(msg)

# TODO: apply to more fields that can be set via environment variables
@field_validator("commit_author", mode="before")
@classmethod
def resolve_env_vars(cls, val: Any) -> Any | str | None:
if isinstance(val, EnvConfigVar):
return val.getvalue()

if not isinstance(val, dict):
return val

try:
return EnvConfigVar.model_validate(val).getvalue()
except ValidationError:
if "env" in val:
raise
return val

@field_serializer("commit_author", mode="plain")
@classmethod
def serialize_commit_author(cls, val: Actor) -> str:
return f"{val.name} <{val.email}>"

@field_validator("repo_dir", mode="after")
@classmethod
def verify_git_repo_dir(cls, dir_path: Path) -> Path:
Expand Down Expand Up @@ -741,16 +812,6 @@ def from_raw_config( # noqa: C901
*(regexp(pattern) for pattern in raw.changelog.exclude_commit_patterns),
)

_commit_author_str = cls.resolve_from_env(raw.commit_author) or ""
_commit_author_valid = Actor.name_email_regex.match(_commit_author_str)
if not _commit_author_valid:
raise ValueError(
f"Invalid git author: {_commit_author_str} "
f"should match {Actor.name_email_regex}"
)

commit_author = Actor(*_commit_author_valid.groups())

version_declarations: list[IVersionReplacer] = []

try:
Expand Down Expand Up @@ -909,7 +970,7 @@ def from_raw_config( # noqa: C901
changelog_mask_initial_release=raw.changelog.default_templates.mask_initial_release,
changelog_insertion_flag=raw.changelog.insertion_flag,
assets=raw.assets,
commit_author=commit_author,
commit_author=raw.commit_author,
commit_message=raw.commit_message,
changelog_excluded_commit_patterns=changelog_excluded_commit_patterns,
# TODO: change when we have other styles per parser
Expand Down
Loading
Loading