diff --git a/.github/workflows/upgrade_deps.yml b/.github/workflows/upgrade_deps.yml new file mode 100644 index 00000000..cdd4b405 --- /dev/null +++ b/.github/workflows/upgrade_deps.yml @@ -0,0 +1,121 @@ +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@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: '3.14' + + - name: Calculate and pin two-week old stable versions + shell: python + run: | + import json + import urllib.request + import re + 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") + + # Regex to find standard dependencies under [project] dependencies = [ ... ] + # This handles lines like "urllib3>=1.26", "cryptography", etc. inside the array + dep_block_match = re.search(r'dependencies\s*=\s*\[(.*?)\]', content, re.DOTALL) + if not dep_block_match: + print("No standard project dependencies array found.") + exit(0) + + dep_block = dep_block_match.group(1) + raw_deps = re.findall(r'"([^"]+)"', dep_block) + + updated_deps = [] + two_weeks_ago = datetime.now(timezone.utc) - timedelta(days=14) + + print(f"Evaluating dependencies against cutoff date: {two_weeks_ago.isoformat()}") + + for dep in raw_deps: + # Isolate base package name (e.g., "urllib3>=1.25" -> "urllib3") + pkg_name = re.split(r'[<>=!~;]', dep)[0].strip() + + # Pull environment markers to re-attach later if they exist (e.g., ; sys_platform == 'win32') + marker = "" + if ";" in dep: + marker = " ; " + dep.split(";", 1)[1].strip() + + print(f"Fetching PyPI data for: {pkg_name}") + try: + url = f"https://pypi.org/pypi/{pkg_name}/json" + req = urllib.request.Request(url, headers={'User-Agent': 'GAM-CI-Script'}) + with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) + + valid_versions = [] + for ver, files in data.get("releases", {}).items(): + if not files: + continue + + # Get upload time of the first file in the release + upload_time_str = files[0].get("upload_time_iso_8601") + if not upload_time_str: + continue + + # Normalize trailing Z to +00:00 for Python's datetime parser + if upload_time_str.endswith('Z'): + upload_time_str = upload_time_str[:-1] + '+00:00' + + upload_time = datetime.fromisoformat(upload_time_str) + + # Filter: Must be older than 2 weeks and not a pre-release (alpha/beta/rc) + if upload_time <= two_weeks_ago and not any(x in ver.lower() for x in ['a', 'b', 'rc', 'dev']): + valid_versions.append((upload_time, ver)) + + if valid_versions: + # Sort by upload time descending to get the newest valid option + valid_versions.sort(key=lambda x: x[0], reverse=True) + target_version = valid_versions[0][1] + + pinned_dep = f"{pkg_name}=={target_version}{marker}" + updated_deps.append(f' "{pinned_dep}",') + print(f" -> Pinned to {target_version}") + else: + # Fallback to original line if no historical versions found + updated_deps.append(f' "{dep}",') + + except Exception as e: + print(f" -> Error fetching package {pkg_name}: {e}. Keeping original constraint.") + updated_deps.append(f' "{dep}",') + + # 2. Reconstruct the dependencies block in pyproject.toml + new_dep_block = "\n" + "\n".join(updated_deps) + "\n" + new_content = content.replace(dep_block_match.group(1), new_dep_block) + toml_path.write_text(new_content, encoding="utf-8") + + - 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 package versions at least 2 weeks old." + branch: sys-deps-upgrade + force: false # Standard push, plays nice with rulesets