My AI agent now opens its own pull requests every Monday A developer released patchpilot v0.2.0 as a GitHub Action that runs weekly, selecting an outdated Python dependency, upgrading it in a Docker sandbox, fixing breaking code until tests pass, and opening a pull request. In its first unattended run it upgraded marshmallow, migrated deprecated field options, and passed all 9 tests. The agent uses deterministic code to pick candidates and skip already-open upgrades, exiting successfully without API calls when there is nothing to do. A few days ago I wrote about patchpilot https://github.com/ilmeedesilva/patchpilot , an agent that upgrades a Python dependency and fixes the code the new version breaks part 1 is here https://dev.to/ilmeedesilva/i-built-an-ai-agent-that-fixes-the-code-a-dependency-upgrade-breaks-2664 . It worked, but it had one honest flaw: you had to remember to run it. And "I'll do the upgrade later" is exactly how dependencies end up two major versions behind. So I made patchpilot a GitHub Action. You add one workflow file, and every Monday it picks an outdated package, upgrades it in a sandbox, fixes the code until the tests pass, and opens a pull request. That's the log from its first real run, replayed. Here's what happened in that run, with nobody at a keyboard: missing= and default= field options, and the test module crashed on import load default= and dump default= , and all 9 tests passed github-actions bot It also flagged a README line that still said "marshmallow 3", and deliberately left it alone to keep the PR focused. That's the judgement I'd want from a human reviewer. Here's the whole setup: name: patchpilot on: schedule: - cron: "0 6 1" every Monday at 06:00 UTC workflow dispatch: adds a "Run workflow" button permissions: contents: write pull-requests: write jobs: upgrade: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: ilmeedesilva/patchpilot@v0.2.0 with: anthropic-api-key: ${{ secrets.ANTHROPIC API KEY }} It's on the GitHub Marketplace https://github.com/marketplace/actions/patchpilot-dependency-upgrader . Getting there took more decisions than I expected. These are the ones worth sharing. The first question is which package to upgrade. I could have asked the model. I didn't, because this decision has a clear right answer, and code gives it for free, instantly, and the same way every time. patchpilot's rule: a package is a candidate when its newest release on PyPI falls outside the range you declared . pydantic =1.10,<2 while 2.x is out? Candidate. An unpinned requests ? Never, because pip already installs the newest version. Among the candidates it picks the smallest jump first patch, then minor, then major , because the safest upgrade is the one most likely to get merged. One rule makes the weekly schedule work: skip any package that already has a patchpilot/upgrade- branch. Without it, every Monday would redo the same upgrade. There was a catch, though. On a GitHub runner, actions/checkout fetches only the default branch, so a local branch check finds nothing. patchpilot asks the remote directly: git ls-remote --heads origin 'patchpilot/upgrade- ' My rule of thumb now: use the model for the parts that need judgement reading a changelog, fixing code . Use code for everything else. A scheduled job that spends money every week to find out there's nothing to do is a job people switch off. So when every outdated package already has an open PR, patchpilot prints "Nothing to upgrade" and exits successfully, without a single API call . The run takes about a second. Exit code 0 matters here too. If "nothing to do" were an error, your Actions tab would turn red every quiet week, and people learn to ignore red. GitHub gives every job a fresh virtual machine that's deleted afterwards. So it was tempting to drop patchpilot's Docker sandbox in CI and run everything directly on the runner. But while the job runs, that machine holds your Anthropic API key and a GitHub token that can push to your repo . Meanwhile it's installing a new version of a package and running a test suite: code you didn't write. A compromised release could read those secrets from the environment in one line. So the sandbox stays. The project's code runs in a container that sees only a temporary copy of the repo, and never the environment the secrets live in. "Throwaway" protects the next job. It doesn't protect this one. ${{ inputs.package }} straight into run: lets a crafted input run shell commands. Every input goes through an environment variable instead. pipx install "$GITHUB ACTION PATH" , not "whatever's latest on PyPI". So @v0.2.0 always means patchpilot 0.2.0. gh auth status . GH TOKEN counts as logged in. To test it I needed a second outdated package in the demo repo, one that really breaks. My first pick was semver , since version 3 was supposed to remove the old module functions. Before building anything, I installed both versions and checked. Version 3 had kept them, only deprecated. That demo would have been an upgrade where nothing broke, and nothing for patchpilot to fix. I switched to marshmallow 4, which really did remove missing= . The other trap: adding marshmallow to requirements.txt would have given my live pydantic PR a merge conflict, since that PR edits the same lines. It went into a separate requirements-api.txt instead. Small, but a "live example" with a red conflict banner isn't much of an example. Even publishing had a lesson. The Marketplace rejected the name "patchpilot" because a GitHub account already has it. The failed release still created the git tag , so it's worth checking for stray tags before you try again. The next goal is the one that matters most: real users . I'll run patchpilot on a few open-source projects with dependencies stuck behind an old major version, review every diff by hand, and ask the maintainers before opening anything . A merged PR in someone else's project is a much better test than any demo. If you'd like to try it, the README https://github.com/ilmeedesilva/patchpilot use-it-in-github-actions has the copy-paste setup. It needs an Anthropic API key, and you can still run it locally with pipx install patchpilot . Feedback is very welcome, especially an upgrade you'd like to see it attempt. I'm on LinkedIn https://www.linkedin.com/in/ilmeedesilva if you'd like to connect.