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
32 changes: 28 additions & 4 deletions .github/workflows/maint-52-sync-dev-versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,24 +183,42 @@ jobs:
id: sync
if: steps.check.outputs.has_dev_deps == 'true'
run: |
set -o pipefail # Ensure pipeline returns first failing command's exit code
set -euo pipefail
# Copy pin file to expected location
mkdir -p consumer/.github/workflows
cp .github/workflows/autofix-versions.env consumer/.github/workflows/

cd consumer

uv_lock_stale=false
if [ -f uv.lock ]; then
python -m pip install --disable-pip-version-check 'uv>=0.5'
if ! uv lock --check; then
uv_lock_stale=true
fi
fi

# Run sync check first
if python ../scripts/sync_dev_dependencies.py \
--check 2>&1 \
| tee /tmp/sync_output.txt; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
if [ "$uv_lock_stale" = "true" ]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
if [ "${{ inputs.dry_run }}" != "true" ]; then
uv lock
fi
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"

# Apply if not dry run
if [ "${{ inputs.dry_run }}" != "true" ]; then
python ../scripts/sync_dev_dependencies.py --apply
if [ -f uv.lock ]; then
uv lock
Comment thread
stranske marked this conversation as resolved.
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
stranske marked this conversation as resolved.
fi
fi

Expand All @@ -213,7 +231,7 @@ jobs:
steps.check.outputs.has_pyproject == 'true'
&& steps.check.outputs.has_dev_deps != 'true'
run: |
set -o pipefail
set -euo pipefail
# Copy pin file to expected location
mkdir -p consumer/.github/workflows
cp .github/workflows/autofix-versions.env consumer/.github/workflows/
Expand All @@ -232,8 +250,12 @@ jobs:
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi
if [ "${{ inputs.dry_run }}" != "true" ] && [ -f uv.lock ]; then
python -m pip install --disable-pip-version-check 'uv>=0.5'
uv lock
fi
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
exit 1
fi

# Save output for PR body
Expand Down Expand Up @@ -277,6 +299,8 @@ jobs:
git add pyproject.toml .github/workflows/autofix-versions.env
if [ -f requirements.lock ]; then git add requirements.lock; fi
if [ -f requirements-dev.lock ]; then git add requirements-dev.lock; fi
if [ -f requirements-dev.txt ]; then git add requirements-dev.txt; fi
if [ -f uv.lock ]; then git add uv.lock; fi

# Commit with multi-line message
commit_msg="deps: sync dev tool versions from Workflows
Expand Down
27 changes: 21 additions & 6 deletions scripts/sync_dev_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@
# Default paths (can be overridden for testing)
PIN_FILE = Path(".github/workflows/autofix-versions.env")
PYPROJECT_FILE = Path("pyproject.toml")
LOCKFILE_FILES = (Path("requirements.lock"), Path("requirements-dev.lock"))
# Direct requirement files can be installed by CI independently of pyproject.
# Keep every supported dev-tool surface aligned with the canonical pin file.
LOCKFILE_FILES = (
Path("requirements.lock"),
Path("requirements-dev.lock"),
Path("requirements-dev.txt"),
Comment thread
stranske marked this conversation as resolved.
)

# Map env file keys to package names
# Format: ENV_KEY -> (package_name, optional_alternative_names)
Expand All @@ -53,7 +59,9 @@
]

LOCKFILE_PATTERN = re.compile(
r"^(?P<lead>\s*)(?P<name>[A-Za-z0-9_.-]+)==(?P<version>[^\s#]+)(?P<trail>\s*(?:#.*)?)$"
r"^(?P<lead>\s*)(?P<name>[A-Za-z0-9_.-]+)(?P<extras>\[[^]]+\])?"
r"(?P<specifier>(?:===|==|!=|<=|>=|~=|<|>)[^\s;#]+)?"
r"(?P<marker>\s*;[^#]+?)?(?P<trail>\s*(?:#.*)?)$"
)


Expand Down Expand Up @@ -335,13 +343,20 @@ def sync_lockfile(
continue

name = match.group("name")
version = match.group("version")
target_version = targets.get(name.lower())
if target_version and version != target_version:
changes.append(f"{lockfile_path.name}:{name}: {version} -> =={target_version}")
current_requirement = f"{match.group('specifier') or ''}{match.group('marker') or ''}"
target_requirement = f"=={target_version}{match.group('marker') or ''}"
if target_version and current_requirement != target_requirement:
current_version = match.group("specifier") or "(unversioned)"
if current_version.startswith("=="):
current_version = current_version[2:]
changes.append(
f"{lockfile_path.name}:{name}: " f"{current_version} -> =={target_version}"
)
if apply:
updated_lines.append(
f"{match.group('lead')}{name}=={target_version}{match.group('trail')}"
f"{match.group('lead')}{name}{match.group('extras') or ''}"
f"=={target_version}{match.group('marker') or ''}{match.group('trail')}"
)
else:
updated_lines.append(line)
Expand Down
27 changes: 27 additions & 0 deletions tests/scripts/test_sync_dev_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,30 @@ def test_sync_lockfile_preserves_comments_and_whitespace(tmp_path: Path) -> None
assert "black==2.0.0" in updated


def test_sync_lockfile_normalizes_non_exact_and_unversioned_requirements(tmp_path: Path) -> None:
lockfile = tmp_path / "requirements-dev.txt"
lockfile.write_text(
"black>=0.2.0\nruff\nmypy[reports]~=1.2.0 ; python_version >= '3.10'\n",
encoding="utf-8",
)

changes, errors = sdd.sync_lockfile(
lockfile,
{"BLACK_VERSION": "2.0.0", "RUFF_VERSION": "1.0.0", "MYPY_VERSION": "1.3.0"},
apply=True,
)

assert errors == []
assert changes == [
"requirements-dev.txt:black: >=0.2.0 -> ==2.0.0",
"requirements-dev.txt:ruff: (unversioned) -> ==1.0.0",
"requirements-dev.txt:mypy: ~=1.2.0 -> ==1.3.0",
]
assert lockfile.read_text(encoding="utf-8") == (
"black==2.0.0\nruff==1.0.0\n" "mypy[reports]==1.3.0 ; python_version >= '3.10'\n"
)


def test_sync_pyproject_normalizes_minimum_pin_at_target_version(
tmp_path: Path,
) -> None:
Expand Down Expand Up @@ -158,12 +182,14 @@ def test_main_apply_updates_all_present_requirements_lockfiles(
pyproject_path = tmp_path / "pyproject.toml"
requirements_lock = tmp_path / "requirements.lock"
requirements_dev_lock = tmp_path / "requirements-dev.lock"
requirements_dev_txt = tmp_path / "requirements-dev.txt"
pins = {"RUFF_VERSION": "1.0.0", "BLACK_VERSION": "2.0.0"}

_write_env_file(env_path, pins)
_write_pyproject(pyproject_path, "0.9.0", "2.0.0")
requirements_lock.write_text("ruff==0.9.0\n", encoding="utf-8")
requirements_dev_lock.write_text("ruff==0.8.0\n", encoding="utf-8")
requirements_dev_txt.write_text("ruff==0.7.0\n", encoding="utf-8")
monkeypatch.chdir(tmp_path)

exit_code = sdd.main(
Expand All @@ -179,6 +205,7 @@ def test_main_apply_updates_all_present_requirements_lockfiles(
assert exit_code == 0
assert requirements_lock.read_text(encoding="utf-8") == "ruff==1.0.0\n"
assert requirements_dev_lock.read_text(encoding="utf-8") == "ruff==1.0.0\n"
assert requirements_dev_txt.read_text(encoding="utf-8") == "ruff==1.0.0\n"


def test_main_check_reports_lockfile_mismatch(
Expand Down
8 changes: 8 additions & 0 deletions tests/workflows/test_maint52_sync_dev_versions_pr_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,11 @@ def test_wave_hash_includes_the_sync_implementation():
assert "scripts/sync_dev_dependencies.py" in hash_block
assert "| sha256sum" in hash_block
assert "| cut -d' ' -f1" in hash_block


def test_dev_version_sync_fails_fast_and_checks_uv_lockfiles():
text = WORKFLOW.read_text(encoding="utf-8")

assert text.count("set -euo pipefail") >= 2
assert "if ! uv lock --check; then" in text
assert "uv_lock_stale=true" in text
Loading