Skip to content

Auto upgrade pyproject constraints #8

Auto upgrade pyproject constraints

Auto upgrade pyproject constraints #8

# .github/workflows/auto-upgrade-pyproject.yml
name: Auto upgrade pyproject constraints
on:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true
- name: Install dependencies for analysis
run: |
python -m pip install --upgrade pip
pip install tomlkit packaging
- name: Backup original files
run: |
cp pyproject.toml pyproject.toml.backup
cp poetry.lock poetry.lock.backup 2>/dev/null || true
- name: Find maximum compatible versions using Poetry resolver
id: find_compatible
run: |
echo "Finding maximum compatible versions for all dependencies..."
# Create a script to systematically find the best compatible versions
cat > find_compatible_versions.py << 'EOF'
import subprocess
import json
import sys
import tomlkit
from pathlib import Path
def run_poetry_command(cmd):
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, check=False)
return result.returncode == 0, result.stdout, result.stderr
except Exception as e:
return False, "", str(e)
def get_outdated_packages():
"""Get list of packages that can be updated."""
success, stdout, stderr = run_poetry_command("poetry show --outdated --format=json")
if not success:
print("Failed to get outdated packages")
return []
try:
data = json.loads(stdout)
return [(pkg["name"], pkg["version"], pkg["latest"]) for pkg in data]
except (json.JSONDecodeError, KeyError):
return []
def test_update_combination(packages_to_update):
"""Test if updating specific packages works."""
if not packages_to_update:
return True
# Try updating just these packages
pkg_list = " ".join(packages_to_update)
success, stdout, stderr = run_poetry_command(f"poetry update --dry-run {pkg_list}")
if success:
# If dry-run succeeds, try actual update
success, stdout, stderr = run_poetry_command(f"poetry update {pkg_list}")
return success
return False
def restore_backup():
"""Restore from backup."""
subprocess.run("cp pyproject.toml.backup pyproject.toml", shell=True)
subprocess.run("cp poetry.lock.backup poetry.lock", shell=True, check=False)
def binary_search_compatible_updates():
"""Use binary search approach to find maximum compatible set."""
outdated = get_outdated_packages()
if not outdated:
print("No packages to update")
return []
print(f"Found {len(outdated)} outdated packages")
package_names = [pkg[0] for pkg in outdated]
# Start with all packages and reduce until we find a working set
left, right = 0, len(package_names)
best_working_set = []
while left <= right:
mid = (left + right) // 2
test_set = package_names[:mid]
print(f"Testing update of {len(test_set)} packages: {test_set}")
# Restore backup before test
restore_backup()
if test_update_combination(test_set):
print(f"✅ Successfully updated {len(test_set)} packages")
best_working_set = test_set.copy()
left = mid + 1
else:
print(f"❌ Failed to update {len(test_set)} packages")
right = mid - 1
return best_working_set
def iterative_compatible_updates():
"""Try adding packages one by one to find maximum compatible set."""
outdated = get_outdated_packages()
if not outdated:
return []
package_names = [pkg[0] for pkg in outdated]
compatible_set = []
for pkg in package_names:
print(f"Testing addition of {pkg}...")
# Restore backup
restore_backup()
# Try updating the current compatible set + this package
test_set = compatible_set + [pkg]
if test_update_combination(test_set):
print(f"✅ {pkg} is compatible, adding to set")
compatible_set.append(pkg)
else:
print(f"❌ {pkg} would cause conflicts, skipping")
return compatible_set
def main():
print("=== Finding Maximum Compatible Package Updates ===")
# Try iterative approach first (more reliable)
compatible_packages = iterative_compatible_updates()
if compatible_packages:
print(f"\n✅ Found {len(compatible_packages)} packages that can be safely updated:")
for pkg in compatible_packages:
print(f" - {pkg}")
# Restore backup and do final update
restore_backup()
if test_update_combination(compatible_packages):
print(f"\n🎉 Successfully updated all {len(compatible_packages)} compatible packages")
return True
else:
print("\n❌ Final update failed unexpectedly")
restore_backup()
return False
else:
print("\n⚠️ No packages can be safely updated due to dependency conflicts")
restore_backup()
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)
EOF
# Run the compatibility finder
if python find_compatible_versions.py; then
echo "compatible_found=true" >> $GITHUB_OUTPUT
echo "✅ Found compatible package updates"
else
echo "compatible_found=false" >> $GITHUB_OUTPUT
echo "❌ No compatible updates found"
fi
- name: Verify final state
if: steps.find_compatible.outputs.compatible_found == 'true'
id: verify_state
run: |
echo "Verifying final dependency state..."
# Test that everything still resolves and installs
if poetry lock --check && poetry install --dry-run; then
echo "verification_success=true" >> $GITHUB_OUTPUT
echo "✅ Final state verification passed"
else
echo "verification_success=false" >> $GITHUB_OUTPUT
echo "❌ Final state verification failed"
cp pyproject.toml.backup pyproject.toml
cp poetry.lock.backup poetry.lock 2>/dev/null || true
fi
- name: Generate comprehensive update summary
if: steps.find_compatible.outputs.compatible_found == 'true' && steps.verify_state.outputs.verification_success == 'true'
id: generate_summary
run: |
# Check what actually changed
if ! git diff --quiet pyproject.toml; then
echo "changes_detected=true" >> $GITHUB_OUTPUT
# Generate detailed summary
cat > update_summary.md << 'EOF'
## 🔄 Maximum Compatible Dependency Updates
This PR contains the **maximum set of dependency updates** that are mutually compatible with each other and all existing constraints.
### 📦 Updated Dependencies
The following constraints were updated using a compatibility-first approach:
```diff
EOF
git diff pyproject.toml >> update_summary.md
cat >> update_summary.md << 'EOF'
```
### 🧪 Validation Process
This update was generated using a smart dependency resolver that:
1. **Identified all outdated packages** in the project
2. **Tested combinations iteratively** to find the maximum compatible set
3. **Ensured global constraint satisfaction** across all dependencies
4. **Verified installation and lock file integrity**
### ✅ Validation Status
- **Dependency Resolution**: ✅ Passed
- **Global Compatibility**: ✅ Passed
- **Lock File Integrity**: ✅ Passed
- **Installation Test**: ✅ Passed
### 🔍 What This Means
- **No dependency conflicts**: All updated packages work together
- **Maximum safe updates**: This is the largest set of updates possible without breaking compatibility
- **Conservative approach**: Skipped packages that would cause conflicts
### 📋 Review Checklist
- [ ] Review the dependency changes above
- [ ] Run full test suite to ensure application compatibility
- [ ] Check changelogs for any behavioral changes in updated packages
- [ ] Verify no breaking changes in patch/minor updates
---
*This PR was automatically generated using a compatibility-first dependency resolver.*
EOF
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
echo "No dependency updates were possible due to compatibility constraints"
fi
- name: Create PR with compatible updates
if: steps.generate_summary.outputs.changes_detected == 'true'
run: |
git checkout -b chore/upgrade-compatible-dependencies
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add pyproject.toml poetry.lock
git commit -m "chore: upgrade dependencies with compatibility validation
- Found maximum compatible set of dependency updates
- All updates validated for global constraint satisfaction
- No dependency conflicts introduced"
git push --force --set-upstream origin chore/upgrade-compatible-dependencies
gh pr create \
--title "🔄 Compatible dependency upgrades" \
--body-file update_summary.md \
--base "${GITHUB_REF_NAME:-main}" \
--label "dependencies,automated,compatible" || true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Report results
if: always()
run: |
if [[ "${{ steps.find_compatible.outputs.compatible_found }}" == "true" && "${{ steps.generate_summary.outputs.changes_detected }}" == "true" ]]; then
echo "✅ Successfully created PR with maximum compatible dependency updates"
elif [[ "${{ steps.find_compatible.outputs.compatible_found }}" == "false" ]]; then
echo "⚠️ No dependency updates possible - all potential updates cause conflicts"
echo "💡 This suggests your current dependency constraints are optimal for compatibility"
else
echo "ℹ️ All dependencies are already at their latest compatible versions"
fi
- name: Cleanup
if: always()
run: |
rm -f pyproject.toml.backup poetry.lock.backup update_summary.md find_compatible_versions.py