Skip to content

Commit 5de1026

Browse files
Enhance auto-upgrade workflow for Python dependencies
Updated the GitHub Actions workflow to install Poetry, backup original files, find maximum compatible versions, and generate a summary for dependency updates.
1 parent 61bc582 commit 5de1026

File tree

1 file changed

+279
-14
lines changed

1 file changed

+279
-14
lines changed

.github/workflows/auto-upgrade-pyproject.yml

Lines changed: 279 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,292 @@ jobs:
2121
with:
2222
python-version: "3.12"
2323

24-
- name: Install deps
24+
- name: Install Poetry
25+
uses: snok/install-poetry@v1
26+
with:
27+
version: latest
28+
virtualenvs-create: true
29+
virtualenvs-in-project: true
30+
31+
- name: Install dependencies for analysis
2532
run: |
2633
python -m pip install --upgrade pip
2734
pip install tomlkit packaging
2835
29-
- name: Run updater (caret, no majors, no pre-releases)
36+
- name: Backup original files
37+
run: |
38+
cp pyproject.toml pyproject.toml.backup
39+
cp poetry.lock poetry.lock.backup 2>/dev/null || true
40+
41+
- name: Find maximum compatible versions using Poetry resolver
42+
id: find_compatible
3043
run: |
31-
python scripts/pyproject_updater.py --strategy caret --groups main,dev --respect-major --no-prerelease
44+
echo "Finding maximum compatible versions for all dependencies..."
45+
46+
# Create a script to systematically find the best compatible versions
47+
cat > find_compatible_versions.py << 'EOF'
48+
import subprocess
49+
import json
50+
import sys
51+
import tomlkit
52+
from pathlib import Path
53+
54+
def run_poetry_command(cmd):
55+
try:
56+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, check=False)
57+
return result.returncode == 0, result.stdout, result.stderr
58+
except Exception as e:
59+
return False, "", str(e)
60+
61+
def get_outdated_packages():
62+
"""Get list of packages that can be updated."""
63+
success, stdout, stderr = run_poetry_command("poetry show --outdated --format=json")
64+
if not success:
65+
print("Failed to get outdated packages")
66+
return []
67+
68+
try:
69+
data = json.loads(stdout)
70+
return [(pkg["name"], pkg["version"], pkg["latest"]) for pkg in data]
71+
except (json.JSONDecodeError, KeyError):
72+
return []
73+
74+
def test_update_combination(packages_to_update):
75+
"""Test if updating specific packages works."""
76+
if not packages_to_update:
77+
return True
78+
79+
# Try updating just these packages
80+
pkg_list = " ".join(packages_to_update)
81+
success, stdout, stderr = run_poetry_command(f"poetry update --dry-run {pkg_list}")
82+
83+
if success:
84+
# If dry-run succeeds, try actual update
85+
success, stdout, stderr = run_poetry_command(f"poetry update {pkg_list}")
86+
return success
87+
return False
88+
89+
def restore_backup():
90+
"""Restore from backup."""
91+
subprocess.run("cp pyproject.toml.backup pyproject.toml", shell=True)
92+
subprocess.run("cp poetry.lock.backup poetry.lock", shell=True, check=False)
93+
94+
def binary_search_compatible_updates():
95+
"""Use binary search approach to find maximum compatible set."""
96+
outdated = get_outdated_packages()
97+
if not outdated:
98+
print("No packages to update")
99+
return []
100+
101+
print(f"Found {len(outdated)} outdated packages")
102+
package_names = [pkg[0] for pkg in outdated]
103+
104+
# Start with all packages and reduce until we find a working set
105+
left, right = 0, len(package_names)
106+
best_working_set = []
107+
108+
while left <= right:
109+
mid = (left + right) // 2
110+
test_set = package_names[:mid]
111+
112+
print(f"Testing update of {len(test_set)} packages: {test_set}")
113+
114+
# Restore backup before test
115+
restore_backup()
116+
117+
if test_update_combination(test_set):
118+
print(f"✅ Successfully updated {len(test_set)} packages")
119+
best_working_set = test_set.copy()
120+
left = mid + 1
121+
else:
122+
print(f"❌ Failed to update {len(test_set)} packages")
123+
right = mid - 1
124+
125+
return best_working_set
126+
127+
def iterative_compatible_updates():
128+
"""Try adding packages one by one to find maximum compatible set."""
129+
outdated = get_outdated_packages()
130+
if not outdated:
131+
return []
132+
133+
package_names = [pkg[0] for pkg in outdated]
134+
compatible_set = []
135+
136+
for pkg in package_names:
137+
print(f"Testing addition of {pkg}...")
138+
139+
# Restore backup
140+
restore_backup()
141+
142+
# Try updating the current compatible set + this package
143+
test_set = compatible_set + [pkg]
144+
145+
if test_update_combination(test_set):
146+
print(f"✅ {pkg} is compatible, adding to set")
147+
compatible_set.append(pkg)
148+
else:
149+
print(f"❌ {pkg} would cause conflicts, skipping")
150+
151+
return compatible_set
152+
153+
def main():
154+
print("=== Finding Maximum Compatible Package Updates ===")
155+
156+
# Try iterative approach first (more reliable)
157+
compatible_packages = iterative_compatible_updates()
158+
159+
if compatible_packages:
160+
print(f"\n✅ Found {len(compatible_packages)} packages that can be safely updated:")
161+
for pkg in compatible_packages:
162+
print(f" - {pkg}")
163+
164+
# Restore backup and do final update
165+
restore_backup()
166+
167+
if test_update_combination(compatible_packages):
168+
print(f"\n🎉 Successfully updated all {len(compatible_packages)} compatible packages")
169+
return True
170+
else:
171+
print("\n❌ Final update failed unexpectedly")
172+
restore_backup()
173+
return False
174+
else:
175+
print("\n⚠️ No packages can be safely updated due to dependency conflicts")
176+
restore_backup()
177+
return False
178+
179+
if __name__ == "__main__":
180+
success = main()
181+
sys.exit(0 if success else 1)
182+
EOF
183+
184+
# Run the compatibility finder
185+
if python find_compatible_versions.py; then
186+
echo "compatible_found=true" >> $GITHUB_OUTPUT
187+
echo "✅ Found compatible package updates"
188+
else
189+
echo "compatible_found=false" >> $GITHUB_OUTPUT
190+
echo "❌ No compatible updates found"
191+
fi
192+
193+
- name: Verify final state
194+
if: steps.find_compatible.outputs.compatible_found == 'true'
195+
id: verify_state
196+
run: |
197+
echo "Verifying final dependency state..."
198+
199+
# Test that everything still resolves and installs
200+
if poetry lock --check && poetry install --dry-run; then
201+
echo "verification_success=true" >> $GITHUB_OUTPUT
202+
echo "✅ Final state verification passed"
203+
else
204+
echo "verification_success=false" >> $GITHUB_OUTPUT
205+
echo "❌ Final state verification failed"
206+
cp pyproject.toml.backup pyproject.toml
207+
cp poetry.lock.backup poetry.lock 2>/dev/null || true
208+
fi
32209
33-
- name: Create PR if changed
210+
- name: Generate comprehensive update summary
211+
if: steps.find_compatible.outputs.compatible_found == 'true' && steps.verify_state.outputs.verification_success == 'true'
212+
id: generate_summary
34213
run: |
35-
if ! git diff --quiet; then
36-
git checkout -b chore/upgrade-pyproject-constraints
37-
git config user.name "github-actions[bot]"
38-
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
39-
git add pyproject.toml
40-
git commit -m "chore: upgrade dependency constraints in pyproject"
41-
git push --force --set-upstream origin chore/upgrade-pyproject-constraints
42-
gh pr create --title "chore: upgrade pyproject constraints" \
43-
--body "Automated update of dependency constraints via PyPI latest." \
44-
--base "${GITHUB_REF_NAME:-main}" || true
214+
# Check what actually changed
215+
if ! git diff --quiet pyproject.toml; then
216+
echo "changes_detected=true" >> $GITHUB_OUTPUT
217+
218+
# Generate detailed summary
219+
cat > update_summary.md << 'EOF'
220+
## 🔄 Maximum Compatible Dependency Updates
221+
222+
This PR contains the **maximum set of dependency updates** that are mutually compatible with each other and all existing constraints.
223+
224+
### 📦 Updated Dependencies
225+
226+
The following constraints were updated using a compatibility-first approach:
227+
228+
```diff
229+
EOF
230+
231+
git diff pyproject.toml >> update_summary.md
232+
233+
cat >> update_summary.md << 'EOF'
234+
```
235+
236+
### 🧪 Validation Process
237+
238+
This update was generated using a smart dependency resolver that:
239+
240+
1. **Identified all outdated packages** in the project
241+
2. **Tested combinations iteratively** to find the maximum compatible set
242+
3. **Ensured global constraint satisfaction** across all dependencies
243+
4. **Verified installation and lock file integrity**
244+
245+
### ✅ Validation Status
246+
247+
- **Dependency Resolution**: ✅ Passed
248+
- **Global Compatibility**: ✅ Passed
249+
- **Lock File Integrity**: ✅ Passed
250+
- **Installation Test**: ✅ Passed
251+
252+
### 🔍 What This Means
253+
254+
- **No dependency conflicts**: All updated packages work together
255+
- **Maximum safe updates**: This is the largest set of updates possible without breaking compatibility
256+
- **Conservative approach**: Skipped packages that would cause conflicts
257+
258+
### 📋 Review Checklist
259+
260+
- [ ] Review the dependency changes above
261+
- [ ] Run full test suite to ensure application compatibility
262+
- [ ] Check changelogs for any behavioral changes in updated packages
263+
- [ ] Verify no breaking changes in patch/minor updates
264+
265+
---
266+
*This PR was automatically generated using a compatibility-first dependency resolver.*
267+
EOF
268+
269+
else
270+
echo "changes_detected=false" >> $GITHUB_OUTPUT
271+
echo "No dependency updates were possible due to compatibility constraints"
45272
fi
273+
274+
- name: Create PR with compatible updates
275+
if: steps.generate_summary.outputs.changes_detected == 'true'
276+
run: |
277+
git checkout -b chore/upgrade-compatible-dependencies
278+
git config user.name "github-actions[bot]"
279+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
280+
git add pyproject.toml poetry.lock
281+
git commit -m "chore: upgrade dependencies with compatibility validation
282+
283+
- Found maximum compatible set of dependency updates
284+
- All updates validated for global constraint satisfaction
285+
- No dependency conflicts introduced"
286+
287+
git push --force --set-upstream origin chore/upgrade-compatible-dependencies
288+
289+
gh pr create \
290+
--title "🔄 Compatible dependency upgrades" \
291+
--body-file update_summary.md \
292+
--base "${GITHUB_REF_NAME:-main}" \
293+
--label "dependencies,automated,compatible" || true
46294
env:
47295
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
296+
297+
- name: Report results
298+
if: always()
299+
run: |
300+
if [[ "${{ steps.find_compatible.outputs.compatible_found }}" == "true" && "${{ steps.generate_summary.outputs.changes_detected }}" == "true" ]]; then
301+
echo "✅ Successfully created PR with maximum compatible dependency updates"
302+
elif [[ "${{ steps.find_compatible.outputs.compatible_found }}" == "false" ]]; then
303+
echo "⚠️ No dependency updates possible - all potential updates cause conflicts"
304+
echo "💡 This suggests your current dependency constraints are optimal for compatibility"
305+
else
306+
echo "ℹ️ All dependencies are already at their latest compatible versions"
307+
fi
308+
309+
- name: Cleanup
310+
if: always()
311+
run: |
312+
rm -f pyproject.toml.backup poetry.lock.backup update_summary.md find_compatible_versions.py

0 commit comments

Comments
 (0)