Inspiration

I've spent too many evenings stuck on React Native packages whose maintainers had moved on. The app worked, the scanner was red, and the fix was in a dependency nobody was releasing anymore. The choices were to live with the advisory, patch node_modules, or start maintaining a fork that is a big headache. Renovate and Dependabot open a pull request and leave the rest to you but if its a major upgrade and needs more than a simple dependency upgrade, they don't work so well. I wanted something that does the rest: fixes what the update breaks, ships it, watches it in production, and takes over the package when its maintainers have stopped.

What it does

Upkeep maintains a fork of an open source project without its upstream maintainers. For the hackathon that's Kutt, an MIT link shortener. Every night GitLab Duo agents sync upstream, bump dependencies, fix what the bumps break, patch vulnerabilities upstream hasn't fixed, and open one merge request. The pipeline tests it, scans it, builds an image with an SBOM, deploys it to Cloud Run behind a staging tag, verifies it, promotes it and watches a canary. If the canary fails, traffic rolls back and an on-call agent opens an incident, writes the postmortem and proposes the revert.

When a fix is blocked by a dependency that stopped releasing, Upkeep adopts it. Kutt's job queue, bull, gets about 918k downloads a week, hasn't released since December 2024, and pins uuid ^8, which has GHSA-w5hq-g745-h8pq. npm audit's only suggested fix was to install bull 1.1.3 from 2016. Upkeep forked bull into its own GitLab project, installed itself there, moved uuid to ^11.1.1, passed bull's own 294 tests and Kutt's queue test against real Redis, published @upkeep/bull, and switched Kutt over through a normal merge request.

How we built it

  • Duo Agent Platform: two custom flows. maintain has four agents (sync, update, audit, ship), and incident investigates failures. The nightly schedule, a failed canary or health check, an adopted package's release, or a mention on an issue can start them.
  • GitLab CI/CD does what the flows' tokens can't. It creates the adopted package's project, turns on auto-merge, publishes packages and starts other flows. Merges wait for green pipelines.
  • GitLab's security scanners (SAST, secret detection, dependency and container scanning) run on every merge request, the image ships with a CycloneDX SBOM, and adopted packages go to the GitLab package registry.
  • Kutt runs on Google Cloud Run with staging tags, promotion, a 12-check canary and automatic rollback. Its SQLite database lives on a Cloud Storage volume.
  • MemoryHQ (https://memoryhq.keronlabs.com/) gives the agents long-term memory of reachability verdicts, human decisions and past adoptions, so each run builds on the last one.
  • upkeep.yml sets how much the agents may do on their own, and UPKEEP.md is a generated ledger of every change the fork carries.

Challenges we ran into

The hackathon sandbox gives Developer access only. We couldn't change project settings, delete projects or start flows from triggers, and "pipelines must succeed" was off, so auto-merge could have merged a red MR. Every handoff had to wait for a green pipeline on its own. A flow's token acts as the person who started it and can't create projects, merge or start pipelines. Those steps moved into CI jobs with a separate token. That token expired once, and the nightly run failed for four nights before we noticed, so the health check now catches failures like that. Flows get no CI/CD variables, so the agents reach MemoryHQ through a small Cloudflare Worker that checks their GitLab token. bull generates its Lua scripts at build time and doesn't commit them. The first release would have shipped without them, so the release job now compares the tarball with the upstream package's file list and refuses to publish if anything is missing.

Accomplishments that we're proud of

The first adoption ran end to end: detection, fork, fix, upstream sync, release, switch, deploy and a green canary. Along the way the fork's agent found and fixed a race in one of bull's own upstream tests.

When production went quiet for one probe, the incident agent read the logs and called it a transient platform failure. It chose not to revert the nodemailer upgrade, because that would have brought back nine high-severity advisories, and it was right.

We measured Upkeep's carbon footprint with SCI and SCI for AI, using real token counts from every agent session. The measurement turned up two pipeline problems, and fixing them cut a maintained day from 28.07 to 13.77 gCO2e.

What we learned

Agents work best behind cheap, deterministic checks. A fetch decides whether production is up, and npm audit and deps.dev decide whether a dependency needs work. The model runs only when one of them finds something, which keeps cost and carbon low.

The hard part of autonomy turned out to be permissions. Most of our design came from working out which token is allowed to do which step.

Measuring also finds bugs. A first-match workflow rule had been running four security scanners every 30 minutes on a commit that never changed, and nobody noticed until we added up the minutes.

What's next for Upkeep

  • Send the fixes back upstream, starting with the bull test race, and switch back to the original package when it releases again.
  • Adopt more packages, starting with the abandoned React Native packages that started this.
  • Let MemoryHQ accept GitLab's OIDC tokens directly, so the proxy Worker can go.
  • Move Kutt's database to Cloud SQL or Litestream once it needs more than one writer.
  • Offer Upkeep as a service, so any team stuck with an abandoned dependency can hand it over.

Built With

Share this project:

Updates

Submission history