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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ jobs:
- name: Check code style with ruff
run: make lint

- name: Reject git-ref dependencies
run: make check-deps

docker-test:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.head_ref != 'release-please--branches--main'
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,11 @@ format-check: # Check code formatting
typecheck: # Check types with mypy
uv run mypy src/

check-deps: # Reject git-ref dependencies in pyproject.toml
uv run python scripts/check_git_deps.py

# Quality gates (used in CI)
quality-check: format-check lint typecheck test-coverage test-handler
quality-check: format-check lint typecheck check-deps test-coverage test-handler

# Code intelligence commands
index: # Generate code intelligence index
Expand Down
7 changes: 2 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ dependencies = [
"huggingface_hub>=0.32.0",
"fastapi>=0.115.0",
"uvicorn[standard]>=0.34.0",
# TEMPORARY (SLS-360 coordinated release): this worker uses
# runpod_flash.runtime.module_loader, which is not in a published runpod-flash
# yet. Pin to the flash branch until runpod/flash#352 is merged and released,
# then REVERT to a released constraint (e.g. "runpod-flash>=1.18.0").
"runpod-flash @ git+https://github.com/runpod/flash.git@deanquinanola/sls-360-local-module-bundling",
# 1.19.0 is the first release containing runpod_flash.runtime.module_loader.
"runpod-flash>=1.19.0",
]

[dependency-groups]
Expand Down
111 changes: 111 additions & 0 deletions scripts/check_git_deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Fail if pyproject.toml declares any dependency from a git ref.

Why this exists: a git-ref dependency carries no version, so the Bump Runtime
Dependencies workflow has nothing to bump and silently stops tracking that
package. Worse, the ref is not immutable -- when the referenced branch is
deleted, every `uv lock` and `uv sync` on main hard-fails with
"couldn't find remote ref". That happened for real: a temporary pin to a flash
feature branch broke main for two weeks (PR #103).

Usage:
python scripts/check_git_deps.py [pyproject.toml]

Exits 0 when clean, 1 when a git dependency is found or the file is unreadable.

Temporary pins during a coordinated cross-repo release are the legitimate use
case this blocks. When you need one, land it behind an explicit, time-boxed
exception rather than deleting this check -- and revert it the moment the
depended-on version is published.
"""

from __future__ import annotations

import sys
import tomllib
from pathlib import Path
from typing import Any

GIT_MARKERS = ("git+", "git://")
DEFAULT_PYPROJECT = "pyproject.toml"


def _is_git_requirement(requirement: str) -> bool:
"""True if a PEP 508 requirement string points at a git repository."""
return any(marker in requirement for marker in GIT_MARKERS)


def _scan_requirements(requirements: Any, label: str) -> list[str]:
"""Collect offending requirement strings from one dependency list."""
if not isinstance(requirements, list):
return []
return [
f"{label}: {req}"
for req in requirements
if isinstance(req, str) and _is_git_requirement(req)
]


def find_git_dependencies(pyproject_text: str) -> list[str]:
"""Return a labelled description of every git-sourced dependency.

Covers the four places a git ref can enter a uv project: project
dependencies, optional-dependency extras, PEP 735 dependency groups, and
`[tool.uv.sources]`.

Returns an empty list when the file declares no git dependencies.
"""
data = tomllib.loads(pyproject_text)
offenders: list[str] = []

project = data.get("project", {})
offenders += _scan_requirements(project.get("dependencies"), "project.dependencies")

for extra, requirements in (project.get("optional-dependencies") or {}).items():
offenders += _scan_requirements(requirements, f"project.optional-dependencies.{extra}")

for group, requirements in (data.get("dependency-groups") or {}).items():
offenders += _scan_requirements(requirements, f"dependency-groups.{group}")

sources = (data.get("tool", {}).get("uv", {}) or {}).get("sources") or {}
for name, source in sources.items():
if isinstance(source, dict) and "git" in source:
offenders.append(f"tool.uv.sources.{name}: git = {source['git']}")

return offenders


def main(argv: list[str] | None = None) -> int:
"""Check a pyproject.toml and report offenders. Returns a process exit code."""
args = argv if argv is not None else sys.argv[1:]
target = Path(args[0]) if args else Path(DEFAULT_PYPROJECT)

try:
text = target.read_text()
except OSError as e:
print(f"error: cannot read {target}: {e}")
return 1

try:
offenders = find_git_dependencies(text)
except tomllib.TOMLDecodeError as e:
print(f"error: cannot parse {target}: {e}")
return 1

if not offenders:
print(f"{target}: no git dependencies")
return 0

print(f"{target}: git dependencies are not allowed on main:")
for offender in offenders:
print(f" {offender}")
print(
"\nA git ref has no version for the bump workflow to track, and breaks "
"every uv lock once the branch is deleted.\n"
"Depend on a published release instead."
)
return 1


if __name__ == "__main__":
sys.exit(main())
153 changes: 153 additions & 0 deletions tests/unit/test_check_git_deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""Tests for the CI guard that rejects git dependencies in pyproject.toml.

A git-ref dependency has no version for the Bump Runtime Dependencies workflow
to bump, so it silently defeats that workflow -- and it hard-fails every
`uv lock` once the referenced branch is deleted. See PR #103.
"""

import importlib.util
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parents[2]


def _load_checker():
"""Load scripts/check_git_deps.py, which is not on the package path."""
path = REPO_ROOT / "scripts" / "check_git_deps.py"
spec = importlib.util.spec_from_file_location("check_git_deps", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


check_git_deps = _load_checker()


class TestFindGitDependencies:
def test_flags_direct_url_git_dependency(self):
pyproject = """
[project]
name = "worker-flash"
dependencies = [
"requests>=2.25.0",
"runpod-flash @ git+https://github.com/runpod/flash.git@some-branch",
]
"""

offenders = check_git_deps.find_git_dependencies(pyproject)

assert offenders == [
"project.dependencies: runpod-flash @ git+https://github.com/runpod/flash.git@some-branch"
]

def test_accepts_released_version_constraints(self):
pyproject = """
[project]
name = "worker-flash"
dependencies = [
"requests>=2.25.0",
"runpod-flash>=1.19.0",
]
"""

assert check_git_deps.find_git_dependencies(pyproject) == []

def test_flags_git_dependency_in_dependency_group(self):
pyproject = """
[project]
name = "worker-flash"
dependencies = []

[dependency-groups]
dev = [
"pytest>=8.3.5",
"some-tool @ git+https://github.com/example/tool.git",
]
"""

offenders = check_git_deps.find_git_dependencies(pyproject)

assert offenders == [
"dependency-groups.dev: some-tool @ git+https://github.com/example/tool.git"
]

def test_flags_optional_dependency_git_ref(self):
pyproject = """
[project]
name = "worker-flash"
dependencies = []

[project.optional-dependencies]
extra = ["thing @ git+ssh://git@github.com/example/thing.git"]
"""

offenders = check_git_deps.find_git_dependencies(pyproject)

assert offenders == [
"project.optional-dependencies.extra: thing @ git+ssh://git@github.com/example/thing.git"
]

def test_flags_tool_uv_sources_git_entry(self):
pyproject = """
[project]
name = "worker-flash"
dependencies = ["runpod"]

[tool.uv.sources]
runpod = { git = "https://github.com/runpod/runpod-python", rev = "main" }
"""

offenders = check_git_deps.find_git_dependencies(pyproject)

assert offenders == [
"tool.uv.sources.runpod: git = https://github.com/runpod/runpod-python"
]

def test_reports_every_offender(self):
pyproject = """
[project]
name = "worker-flash"
dependencies = [
"a @ git+https://github.com/example/a.git",
"b @ git+https://github.com/example/b.git",
]
"""

assert len(check_git_deps.find_git_dependencies(pyproject)) == 2


class TestMain:
def test_exits_nonzero_and_names_offender_when_git_dep_present(self, tmp_path, capsys):
target = tmp_path / "pyproject.toml"
target.write_text(
'[project]\nname = "x"\ndependencies = ["p @ git+https://github.com/e/p.git"]\n'
)

exit_code = check_git_deps.main([str(target)])

assert exit_code == 1
assert "git+https://github.com/e/p.git" in capsys.readouterr().out

def test_exits_zero_for_clean_file(self, tmp_path):
target = tmp_path / "pyproject.toml"
target.write_text('[project]\nname = "x"\ndependencies = ["requests>=2.25.0"]\n')

assert check_git_deps.main([str(target)]) == 0

def test_exits_nonzero_when_file_missing(self, tmp_path):
assert check_git_deps.main([str(tmp_path / "nope.toml")]) == 1


class TestThisRepository:
"""The guard must pass against the real pyproject.toml it protects."""

def test_repo_pyproject_has_no_git_dependencies(self):
pyproject = (REPO_ROOT / "pyproject.toml").read_text()

assert check_git_deps.find_git_dependencies(pyproject) == []


if __name__ == "__main__":
pytest.main([__file__])
18 changes: 13 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.