diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2da6ee94..ba42d7b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,6 +69,8 @@ jobs: if: ${{ needs.create-metadata.outputs.npm_packages != '[]' || needs.create-metadata.outputs.pypi_packages != '[]' }} runs-on: ubuntu-latest environment: release + permissions: + contents: write outputs: changes_made: ${{ steps.commit.outputs.changes_made }} steps: @@ -130,7 +132,7 @@ jobs: - name: Install dependencies working-directory: src/${{ matrix.package }} - run: uv sync --locked --all-extras --dev + run: uv sync --frozen --all-extras --dev - name: Run pyright working-directory: src/${{ matrix.package }} diff --git a/scripts/release.py b/scripts/release.py index 05d76c0a..e4ce1274 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -97,6 +97,9 @@ class PyPiPackage: with open(self.path / "pyproject.toml", "w") as f: f.write(tomlkit.dumps(data)) + # Regenerate uv.lock to match the updated pyproject.toml + subprocess.run(["uv", "lock"], cwd=self.path, check=True) + def has_changes(path: Path, git_hash: GitHash) -> bool: """Check if any files changed between current state and git hash""" diff --git a/src/git/src/mcp_server_git/server.py b/src/git/src/mcp_server_git/server.py index 23e9b53f..58d8178d 100644 --- a/src/git/src/mcp_server_git/server.py +++ b/src/git/src/mcp_server_git/server.py @@ -13,6 +13,7 @@ from mcp.types import ( ) from enum import Enum import git +from git.exc import BadName from pydantic import BaseModel, Field # Default number of context lines to show in diff output @@ -119,7 +120,7 @@ def git_diff(repo: git.Repo, target: str, context_lines: int = DEFAULT_CONTEXT_L # Defense in depth: reject targets starting with '-' to prevent flag injection, # even if a malicious ref with that name exists (e.g. via filesystem manipulation) if target.startswith("-"): - raise git.exc.BadName(f"Invalid target: '{target}' - cannot start with '-'") + raise BadName(f"Invalid target: '{target}' - cannot start with '-'") repo.rev_parse(target) # Validates target is a real git ref, throws BadName if not return repo.git.diff(f"--unified={context_lines}", target) @@ -187,7 +188,7 @@ def git_checkout(repo: git.Repo, branch_name: str) -> str: # Defense in depth: reject branch names starting with '-' to prevent flag injection, # even if a malicious ref with that name exists (e.g. via filesystem manipulation) if branch_name.startswith("-"): - raise git.exc.BadName(f"Invalid branch name: '{branch_name}' - cannot start with '-'") + raise BadName(f"Invalid branch name: '{branch_name}' - cannot start with '-'") repo.rev_parse(branch_name) # Validates branch_name is a real git ref, throws BadName if not repo.git.checkout(branch_name) return f"Switched to branch '{branch_name}'" diff --git a/src/git/tests/test_server.py b/src/git/tests/test_server.py index 3dba7387..054bf8c7 100644 --- a/src/git/tests/test_server.py +++ b/src/git/tests/test_server.py @@ -1,6 +1,7 @@ import pytest from pathlib import Path import git +from git.exc import BadName from mcp_server_git.server import ( git_checkout, git_branch, @@ -40,7 +41,7 @@ def test_git_checkout_existing_branch(test_repository): def test_git_checkout_nonexistent_branch(test_repository): - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_checkout(test_repository, "nonexistent-branch") def test_git_branch_local(test_repository): @@ -316,25 +317,25 @@ def test_validate_repo_path_symlink_escape(tmp_path: Path): def test_git_diff_rejects_flag_injection(test_repository): """git_diff should reject flags that could be used for argument injection.""" - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_diff(test_repository, "--output=/tmp/evil") - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_diff(test_repository, "--help") - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_diff(test_repository, "-p") def test_git_checkout_rejects_flag_injection(test_repository): """git_checkout should reject flags that could be used for argument injection.""" - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_checkout(test_repository, "--help") - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_checkout(test_repository, "--orphan=evil") - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_checkout(test_repository, "-f") @@ -398,7 +399,7 @@ def test_git_diff_rejects_malicious_refs(test_repository): malicious_ref_path.write_text(sha) # Even though the ref exists, it should be rejected - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_diff(test_repository, "--output=evil.txt") # Verify no file was created (the attack was blocked) @@ -417,7 +418,7 @@ def test_git_checkout_rejects_malicious_refs(test_repository): malicious_ref_path.write_text(sha) # Even though the ref exists, it should be rejected - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_checkout(test_repository, "--orphan=evil") # Cleanup