mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-29 18:31:38 +00:00
153 lines
5.7 KiB
YAML
153 lines
5.7 KiB
YAML
name: Daily Dependency Pinning (2-Week Buffer)
|
|
|
|
on:
|
|
schedule:
|
|
# Runs every day at midnight UTC
|
|
- cron: '0 0 * * *'
|
|
workflow_dispatch: # Allows you to trigger it manually from the UI
|
|
|
|
jobs:
|
|
pin-deps:
|
|
runs-on: ubuntu-slim
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
|
|
steps:
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
|
|
with:
|
|
python-version: '3.14'
|
|
|
|
- name: Install uv
|
|
uses: astral-sh/setup-uv@v3
|
|
with:
|
|
version: "latest"
|
|
|
|
- name: Calculate and pin two-week old stable versions
|
|
shell: python
|
|
run: |
|
|
import subprocess
|
|
import re
|
|
import tomllib
|
|
import os
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
toml_path = Path("pyproject.toml")
|
|
if not toml_path.exists():
|
|
print("pyproject.toml not found!")
|
|
exit(1)
|
|
|
|
content = toml_path.read_text(encoding="utf-8")
|
|
parsed_toml = tomllib.loads(content)
|
|
|
|
target_deps = set()
|
|
|
|
# Gather all dependencies
|
|
target_deps.update(parsed_toml.get("project", {}).get("dependencies", []))
|
|
for deps in parsed_toml.get("project", {}).get("optional-dependencies", {}).values():
|
|
target_deps.update(deps)
|
|
for deps in parsed_toml.get("dependency-groups", {}).values():
|
|
if isinstance(deps, list):
|
|
for d in deps:
|
|
if isinstance(d, str):
|
|
target_deps.add(d)
|
|
|
|
if not target_deps:
|
|
print("No dependencies found to process.")
|
|
exit(0)
|
|
|
|
# 1. Create a "clean" requirements list (base names + markers only, no versions)
|
|
clean_reqs = []
|
|
for dep in target_deps:
|
|
pkg_base = re.split(r'[<>=!~;\s]', dep)[0].strip()
|
|
if ";" in dep:
|
|
marker = dep.split(";", 1)[1].strip()
|
|
clean_reqs.append(f"{pkg_base} ; {marker}")
|
|
else:
|
|
clean_reqs.append(pkg_base)
|
|
|
|
temp_in = Path("temp_reqs.in")
|
|
temp_in.write_text("\n".join(clean_reqs), encoding="utf-8")
|
|
|
|
# 2. Calculate Cutoff Date
|
|
two_weeks_ago = datetime.now(timezone.utc) - timedelta(days=14)
|
|
cutoff_str = two_weeks_ago.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
print(f"Resolving dependencies against cutoff date: {cutoff_str}")
|
|
|
|
# 3. Use uv to resolve the CLEAN list, allowing free upgrades/downgrades
|
|
try:
|
|
subprocess.run([
|
|
"uv", "pip", "compile",
|
|
str(temp_in),
|
|
"--exclude-newer", cutoff_str,
|
|
"--quiet",
|
|
"-o", "resolved.txt"
|
|
], check=True)
|
|
except subprocess.CalledProcessError:
|
|
print("\nDependency resolution failed! Upstream constraints are impossible to satisfy.")
|
|
if temp_in.exists(): temp_in.unlink()
|
|
exit(1)
|
|
|
|
# 4. Parse the resolved lockfile
|
|
resolved_versions = {}
|
|
with open("resolved.txt", "r", encoding="utf-8") as f:
|
|
for line in f:
|
|
line = line.split("#")[0].strip()
|
|
if "==" in line:
|
|
pkg, ver = line.split("==", 1)
|
|
resolved_versions[pkg.strip().lower()] = ver.strip()
|
|
|
|
# Cleanup temp files so they don't get committed to the PR
|
|
if temp_in.exists():
|
|
temp_in.unlink()
|
|
if Path("resolved.txt").exists():
|
|
Path("resolved.txt").unlink()
|
|
|
|
# 5. Map the newly resolved versions back to your pyproject.toml updates
|
|
updates = {}
|
|
for dep in target_deps:
|
|
pkg_name_raw = re.split(r'[<>=!~;\s]', dep)[0].strip()
|
|
pkg_name_lower = pkg_name_raw.lower()
|
|
|
|
marker = ""
|
|
if ";" in dep:
|
|
marker = " ; " + dep.split(";", 1)[1].strip()
|
|
|
|
if pkg_name_lower in resolved_versions:
|
|
target_version = resolved_versions[pkg_name_lower]
|
|
pinned_dep = f"{pkg_name_raw}=={target_version}{marker}"
|
|
|
|
if pinned_dep != dep:
|
|
updates[dep] = pinned_dep
|
|
print(f" -> Changing: '{dep}' => '{pinned_dep}'")
|
|
else:
|
|
print(f" -> Up to date: {dep}")
|
|
|
|
# 6. Replace the strings safely in the original file content
|
|
new_content = content
|
|
for old_dep, new_dep in updates.items():
|
|
escaped_old = re.escape(old_dep)
|
|
pattern = r'([\'"])' + escaped_old + r'\1'
|
|
new_content = re.sub(pattern, lambda m: m.group(1) + new_dep + m.group(1), new_content)
|
|
|
|
if content != new_content:
|
|
toml_path.write_text(new_content, encoding="utf-8")
|
|
print("\npyproject.toml updated successfully.")
|
|
else:
|
|
print("\nNo updates required.")
|
|
|
|
- name: Create Pull Request
|
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1
|
|
with:
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
commit-message: "chore: upgrade PyPi deps"
|
|
title: "Upgrade PyPi deps"
|
|
body: "Automated scan checking PyPI for mutually compatible package versions at least 2 weeks old. Handles both upgrades and conflict-driven downgrades."
|
|
branch: sys-deps-upgrade
|
|
force: false # Standard push, plays nice with rulesets
|