Batch Dependency Updates #113
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Batch Dependency Updates | |
on: | |
schedule: | |
- cron: '0 0 * * *' | |
workflow_dispatch: | |
env: | |
BRANCH_NAME: deps/batch-updates | |
jobs: | |
batch-update: | |
runs-on: ubuntu-latest | |
permissions: | |
contents: write | |
pull-requests: write | |
steps: | |
- uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
- name: Set up Git user | |
run: | | |
git config --global user.name "AshokShau" | |
git config --global user.email "[email protected]" | |
- name: Set up Python | |
uses: actions/setup-python@v4 | |
with: | |
python-version: '3.11' | |
- name: Install dependencies | |
run: | | |
python -m pip install --upgrade pip | |
pip install uv jq tomli tomli-w packaging | |
uv venv .venv | |
source .venv/bin/activate | |
uv pip install tomli tomli-w packaging | |
- name: Get package versions | |
id: get-versions | |
run: | | |
source .venv/bin/activate | |
uv pip install -e . | |
ALL_PKGS=$(uv pip list --format=json) | |
OUTDATED=$(uv pip list --outdated --format=json) | |
VERSION_MAP=$(jq -n --argjson all "$ALL_PKGS" --argjson outdated "$OUTDATED" ' | |
($all | map({(.name): {version: .version}})) + | |
($outdated | map({(.name): {latest_version: .latest_version}})) | |
| add | |
| with_entries(.value |= (.version // .latest_version // "")) | |
') | |
ENCODED_VERS=$(echo "$VERSION_MAP" | base64 -w0) | |
echo "versions_b64=${ENCODED_VERS}" >> $GITHUB_OUTPUT | |
COUNT=$(echo "$OUTDATED" | jq -r 'length') | |
echo "count=${COUNT}" >> $GITHUB_OUTPUT | |
PKG_MD=$(echo "$OUTDATED" | jq -r '.[] | "| \(.name) | \(.version) | \(.latest_version) |"') | |
echo "pkg_list_markdown<<EOF" >> $GITHUB_OUTPUT | |
echo "$PKG_MD" >> $GITHUB_OUTPUT | |
echo "EOF" >> $GITHUB_OUTPUT | |
- name: Update pyproject.toml | |
id: update-deps | |
run: | | |
source .venv/bin/activate | |
python <<EOF | |
import json, base64 | |
import tomli, tomli_w | |
from packaging.requirements import Requirement | |
version_map = json.loads(base64.b64decode("${{ steps.get-versions.outputs.versions_b64 }}")) | |
with open("pyproject.toml", "rb") as f: | |
pyproject = tomli.load(f) | |
updated_packages = [] | |
state = {"updated": False} | |
def process_deps(deps): | |
for i, dep in enumerate(deps): | |
try: | |
req = Requirement(dep) | |
spec = next(iter(req.specifier), None) | |
op = spec.operator if spec else None | |
pkg_name = req.name.lower() | |
if pkg_name in version_map and version_map[pkg_name]: | |
old_version = spec.version if spec else "?" | |
new_version = version_map[pkg_name] | |
if str(old_version) == str(new_version): | |
continue | |
new_op = op if op else "~=" | |
deps[i] = f"{req.name}{new_op}{new_version}" | |
updated_packages.append({ | |
"name": req.name, | |
"old": str(old_version), | |
"new": str(new_version) | |
}) | |
state["updated"] = True | |
except Exception as e: | |
print(f"Skipping invalid requirement {dep}: {e}") | |
process_deps(pyproject["project"]["dependencies"]) | |
if "optional-dependencies" in pyproject["project"]: | |
for group in pyproject["project"]["optional-dependencies"].values(): | |
process_deps(group) | |
if state["updated"]: | |
with open("pyproject.toml", "wb") as f: | |
tomli_w.dump(pyproject, f) | |
print("::set-output name=updates_made::true") | |
print("::set-output name=updated_packages::" + json.dumps(updated_packages)) | |
else: | |
print("::set-output name=updates_made::false") | |
print("::set-output name=updated_packages::[]") | |
EOF | |
- name: Get clean diff of changes | |
id: get-diff | |
run: | | |
echo "diff<<EOF" >> $GITHUB_OUTPUT | |
git diff -U0 pyproject.toml | grep '^[+-][^+-]' >> $GITHUB_OUTPUT || true | |
echo "EOF" >> $GITHUB_OUTPUT | |
- name: Sync lockfile | |
if: steps.update-deps.outputs.updates_made == 'true' | |
run: | | |
source .venv/bin/activate | |
uv sync --upgrade | |
echo "Lockfile updated via uv sync" | |
- name: Generate commit message | |
if: steps.update-deps.outputs.updates_made == 'true' | |
id: commit-message | |
run: | | |
COUNT=$(echo '${{ steps.update-deps.outputs.updated_packages }}' | jq length) | |
echo "commit_title=chore(deps): update $COUNT packages" >> $GITHUB_OUTPUT | |
CHANGES=$(echo '${{ steps.update-deps.outputs.updated_packages }}' | jq -r '.[] | "- \(.name) \(.old) → \(.new)"') | |
echo "commit_body<<EOF" >> $GITHUB_OUTPUT | |
echo "$CHANGES" >> $GITHUB_OUTPUT | |
echo "EOF" >> $GITHUB_OUTPUT | |
- name: Create Pull Request | |
if: steps.update-deps.outputs.updates_made == 'true' | |
uses: peter-evans/create-pull-request@v5 | |
with: | |
title: "${{ steps.commit-message.outputs.commit_title }}" | |
body: | | |
Automated dependency updates: | |
${{ steps.commit-message.outputs.commit_body }} | |
Diff: | |
```diff | |
${{ steps.get-diff.outputs.diff }} | |
``` | |
branch: "${{ env.BRANCH_NAME }}" | |
commit-message: "${{ steps.commit-message.outputs.commit_title }}" | |
committer: "AshokShau <[email protected]>" | |
author: "AshokShau <[email protected]>" | |
delete-branch: false | |
branch-suffix: "" | |
title-prefix: "" | |
- name: No updates found | |
if: steps.update-deps.outputs.updates_made == 'false' | |
run: | | |
echo "No dependency updates available at this time." |