diff --git a/scripts/bump_version.py b/scripts/bump_version.py index 94db003..23b946f 100755 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -8,6 +8,7 @@ import shutil import subprocess import sys +import urllib.parse from datetime import datetime from pathlib import Path @@ -153,12 +154,36 @@ def git_commit(version: str, files: list[str] | None = None, cwd: Path | None = print(f"✅ Git commit on {release_branch(version)}: chore: bump version to {version}") +def _github_repo(url: str) -> str | None: + """owner/name when the remote URL points at github.com, otherwise None. + + The host and the path are read apart and compared whole: a substring test lets + any host that mentions github.com in its path or its own name through (CodeQL + py/incomplete-url-substring-sanitization, alert #60). + """ + url = url.strip() + if "://" in url: + parts = urllib.parse.urlsplit(url) + host, path = parts.hostname or "", parts.path + else: + # scp-like syntax: [user@]host:path + scp = re.fullmatch(r"(?:[^@/:]+@)?([^@/:]+):(.+)", url) + if not scp: + return None + host, path = scp.group(1).lower(), scp.group(2) + if host != "github.com": + return None + path = path.strip("/") + return path.removesuffix(".git") or None + + def _origin_is_github(cwd: Path | None) -> bool: url = subprocess.run( ["git", "remote", "get-url", "origin"], capture_output=True, text=True, encoding="utf-8", check=True, cwd=cwd, ).stdout - return "github.com" in url and RELEASE_REPO in url + repo = _github_repo(url) + return repo is not None and repo.lower() == RELEASE_REPO.lower() def git_push(version: str, cwd: Path | None = None) -> None: diff --git a/tests/test_release_through_pr.py b/tests/test_release_through_pr.py index 5832e13..c204fc1 100644 --- a/tests/test_release_through_pr.py +++ b/tests/test_release_through_pr.py @@ -114,3 +114,47 @@ def test_the_bump_is_committed_on_a_release_branch_and_main_is_left_alone(repo): assert _git(remote, "log", "-1", "--format=%s", "release/2026.47") == "chore: bump version to 2026.47" assert _git(remote, "log", "-1", "--format=%s", "main") == "initial", "main was pushed" assert _git(work, "rev-parse", "--abbrev-ref", "HEAD") == "main" + + +# --- who opens the pull request ---------------------------------------------------- +# +# The pull request is opened with gh, with auto-merge, only when origin is this +# repository on GitHub. CodeQL alert #60 (py/incomplete-url-substring-sanitization) +# was right about the first version: it asked whether "github.com" and the repository +# name occurred anywhere in the URL, which a host of anyone's choosing satisfies. +# The URLs below are set on a real clone with `git remote set-url`; git does not +# contact them to answer `git remote get-url`. + + +@pytest.mark.parametrize("url", [ + "https://github.com/maksimtech/patchradar.git", + "https://github.com/maksimtech/patchradar", + "https://GitHub.com/maksimtech/patchradar.git", + "git@github.com:maksimtech/patchradar.git", + "ssh://git@github.com/maksimtech/patchradar.git", +]) +def test_origin_on_github_is_recognised(repo, url): + work, _ = repo + _git(work, "remote", "set-url", "origin", url) + assert bump._origin_is_github(work) + + +@pytest.mark.parametrize("url", [ + "https://evil.example/github.com/maksimtech/patchradar.git", + "https://github.com.evil.example/maksimtech/patchradar.git", + "https://evil.example/?github.com/maksimtech/patchradar", + "git@evil.example:github.com/maksimtech/patchradar.git", + "https://github.com/maksimtech/patchradar-fork.git", + "https://github.com/someone/maksimtech/patchradar.git", + "https://github.com/someone-else/patchradar.git", +]) +def test_origin_that_only_mentions_github_is_not_trusted(repo, url): + work, _ = repo + _git(work, "remote", "set-url", "origin", url) + assert not bump._origin_is_github(work) + + +def test_a_local_origin_is_not_github(repo): + work, remote = repo + assert _git(work, "remote", "get-url", "origin") == str(remote) + assert not bump._origin_is_github(work)