Skip to content

Batch Dependency Updates #113

Batch Dependency Updates

Batch Dependency Updates #113

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."