{"slug": "my-ai-agent-now-opens-its-own-pull-requests-every-monday", "title": "My AI agent now opens its own pull requests every Monday", "summary": "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.", "body_md": "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.\n\nSo 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.\n\n*That's the log from its first real run, replayed.*\n\nHere's what happened in that run, with nobody at a keyboard:\n\n`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]`\nIt 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.\n\nHere's the whole setup:\n\n```\nname: patchpilot\n\non:\n  schedule:\n    - cron: \"0 6 * * 1\"  # every Monday at 06:00 UTC\n  workflow_dispatch:      # adds a \"Run workflow\" button\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  upgrade:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n      - uses: ilmeedesilva/patchpilot@v0.2.0\n        with:\n          anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}\n```\n\nIt'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.\n\nThe 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.\n\npatchpilot'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.\n\nOne 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:\n\n```\ngit ls-remote --heads origin 'patchpilot/upgrade-*'\n```\n\nMy rule of thumb now: **use the model for the parts that need judgement** (reading a changelog, fixing code). **Use code for everything else.**\n\nA 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.\n\nExit 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.\n\nGitHub 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.\n\nBut 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.\n\nSo 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.\n\n`${{ 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.\nTo 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=`.\n\nThe 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.\n\nEven 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.\n\nThe 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.\n\nIf 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.\n\n*I'm on [LinkedIn](https://www.linkedin.com/in/ilmeedesilva) if you'd like to connect.*", "url": "https://wpnews.pro/news/my-ai-agent-now-opens-its-own-pull-requests-every-monday", "canonical_source": "https://dev.to/ilmeedesilva/my-ai-agent-now-opens-its-own-pull-requests-every-monday-4fji", "published_at": "2026-11-03 02:09:56+00:00", "updated_at": "2026-11-03 02:22:19.506356+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools", "artificial-intelligence"], "entities": ["patchpilot", "GitHub", "PyPI", "Anthropic", "marshmallow", "pydantic"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/my-ai-agent-now-opens-its-own-pull-requests-every-monday", "markdown": "https://wpnews.pro/news/my-ai-agent-now-opens-its-own-pull-requests-every-monday.md", "text": "https://wpnews.pro/news/my-ai-agent-now-opens-its-own-pull-requests-every-monday.txt", "jsonld": "https://wpnews.pro/news/my-ai-agent-now-opens-its-own-pull-requests-every-monday.jsonld"}}