From 420cd4713e3147309ea01db339a91e90b899985b Mon Sep 17 00:00:00 2001 From: codejedi365 Date: Fri, 3 Jul 2026 12:43:23 -0600 Subject: [PATCH 1/2] test(helpers): add unit test to exemplify logging of git credentials --- tests/unit/semantic_release/test_helpers.py | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/unit/semantic_release/test_helpers.py b/tests/unit/semantic_release/test_helpers.py index 4877d3892..5909d49fd 100644 --- a/tests/unit/semantic_release/test_helpers.py +++ b/tests/unit/semantic_release/test_helpers.py @@ -1,4 +1,5 @@ from typing import Iterable +from unittest.mock import patch import pytest @@ -120,6 +121,34 @@ def test_parse_valid_git_urls(url: str, expected: ParsedGitUrl): assert expected == parse_git_url(url) +def test_parse_git_url_does_not_log_credentials(): + """Test that credentials in git urls are masked before logging.""" + username = "x-oauth-basic" + secret = "ghp_secret_token" + url = f"https://{username}:{secret}@github.example.com/owner/project.git" + + expected_parsed_url = ParsedGitUrl( + "https", + f"{username}:{secret}@github.example.com", + "owner", + "project", + ) + + parse_git_url.cache_clear() + with patch("semantic_release.helpers.log") as mock_log: + actual_parsed_url = parse_git_url(url) + + assert expected_parsed_url == actual_parsed_url + assert mock_log.debug.called + + for debug_call in mock_log.debug.call_args_list: + args = debug_call[0] + formatted_msg = args[0] % args[1:] if len(args) > 1 else str(args[0]) + assert secret not in formatted_msg + assert username not in formatted_msg + assert "@github.example.com" in formatted_msg + + @pytest.mark.parametrize( "url", [ From 3e61d003eeb392404345d5a7c0006fe977a40368 Mon Sep 17 00:00:00 2001 From: snoopuppy582 Date: Thu, 14 May 2026 02:53:17 +0900 Subject: [PATCH 2/2] fix(hvcs): mask credentials in git URL debug logs Resolves: #1426 --- src/semantic_release/helpers.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/semantic_release/helpers.py b/src/semantic_release/helpers.py index 5f6723ec4..d8487fda5 100644 --- a/src/semantic_release/helpers.py +++ b/src/semantic_release/helpers.py @@ -10,7 +10,7 @@ from pathlib import Path, PurePosixPath from re import IGNORECASE, compile as regexp from typing import TYPE_CHECKING, Any, Callable, NamedTuple, Sequence, TypeVar -from urllib.parse import urlsplit +from urllib.parse import urlsplit, urlunsplit if TYPE_CHECKING: # pragma: no cover from re import Pattern @@ -215,6 +215,16 @@ class ParsedGitUrl(NamedTuple): repo_name: str +def _hide_credentials_in_url(url: str) -> str: + url_parts = urlsplit(url) + + if not url_parts.scheme or "@" not in url_parts.netloc: + return url + + _, _, host = url_parts.netloc.rpartition("@") + return urlunsplit(url_parts._replace(netloc=f"@{host}")) + + @lru_cache(maxsize=512) def parse_git_url(url: str) -> ParsedGitUrl: """ @@ -242,7 +252,7 @@ def parse_git_url(url: str) -> ParsedGitUrl: Raises ValueError if the url can't be parsed. """ - log.debug("Parsing git url %r", url) + log.debug("Parsing git url %r", _hide_credentials_in_url(url)) # Normalizers are a list of tuples of (pattern, replacement) normalizers = [