# Upgrade Go Modules in a Go CLI

Yes. Each week an agent runs go get -u in groups on a Go CLI, builds, vets, race-tests and runs govulncheck after each, and opens one pull request.

[Run this on Rush](https://cloud.getrush.ai/run/upgrade-go-modules/go-cli/), in a Project that has your repository.

## What the agent does

The agent reads `go.mod` to see what moves together. In charmbracelet/glow, module `charm.land/glow/v3`, the `go` directive is `1.26.7` and there are two dozen direct requirements. Four are the Charm v2 libraries on the `charm.land` path: `bubbles/v2` v2.1.1, `bubbletea/v2` v2.0.8, `glamour/v2` v2.0.1 and `lipgloss/v2` v2.0.6, which share internals and move as a set. Beside them are `spf13/cobra` v1.10.2 and `spf13/viper` v1.21.0, `charmbracelet/log` v0.4.2, `caarlos0/env/v11`, `fsnotify`, `go-runewidth`, `uniseg`, `sahilm/fuzzy`, `mvdan.cc/sh/v3`, and the `golang.org/x` modules `sys`, `term` and `text`. The indirect block carries pseudo-versions that matter: `charmbracelet/ultraviolet` at a dated `v0.0.0` and the old `charmbracelet/lipgloss` at a `v1.1.1-0` pre-release, both pulled in by the v2 libraries, so a Charm bump can move them to a tagged release or to a newer pseudo-version.

Then it reads what the repository already does about upgrades, so the run agrees with it. `.github/dependabot.yml` groups all `gomod` updates into one monthly pull request and ignores `bubbletea/v2` at `v2.0.0-beta1`, and `dependabot-sync.yml` refreshes that config weekly from `charmbracelet/meta`. `Taskfile.yaml` defines `test` as `go test ./...` and `lint` as `golangci-lint run`. `.github/workflows/coverage.yml` runs `go test -race -covermode atomic -coverprofile=profile.cov ./...`, `build.yml` reuses the shared build, snapshot, `govulncheck`, semgrep and ruleguard workflows, and `lint.yml` runs golangci-lint with `--issues-exit-code=0`, so lint is advisory there. The tests are `glow_test.go`, `url_test.go` with its network cases skipped, `ui/pager_test.go`, `ui/ui_test.go`, `ui/rawpaint_test.go` and `utils/textsize_test.go`.

The groups follow the module graph. The Charm set moves first and together, with `go get` naming all four, followed by `go mod tidy` and a check of what happened to `ultraviolet` and the old `lipgloss` in the indirect block; a Bubble Tea or Lip Gloss release that changes an API the `ui/` package uses shows up as a compile error and sends the group back. Cobra, Viper and `pflag` move as the second group, since a Viper change can alter how `glow.yml` keys bind to flags in `main.go`. The `golang.org/x` modules move as the third, and everything else as the fourth. The `go` directive stays at `1.26.7` unless a required module demands a newer one, and then the pull request says which module and why. Ignore rules from `dependabot.yml` are respected, so a version Dependabot is told to skip is skipped here too.

After each group the agent runs `go build ./...`, `go vet ./...`, `go test -race ./...`, `govulncheck ./...` and `golangci-lint run`, and renders a README through the built binary with `--style notty` into a file to see that output still appears. A group that passes is a commit; a group that fails is reverted with the error kept. The pull request lists each bump with old and new versions, including indirect modules that moved, the skips with their reason, and the five check results.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Upgrade this Go CLI's modules in groups: the charm.land v2 libraries together, then spf13/cobra, spf13/viper and spf13/pflag, then the golang.org/x modules, then everything else, each with go get followed by go mod tidy. Respect the ignore rules in .github/dependabot.yml and keep the go directive unless a module requires a newer one, and say which. After each group run go build ./..., go vet ./..., go test -race ./..., govulncheck ./... and golangci-lint run, and render README.md through the built binary with --style notty to a file. Keep a group only if everything passes; otherwise revert it and record the error. Open one pull request with one commit per group, one line per bump with old and new versions including indirect modules that moved, and every upgrade left out with the reason.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on a new branch that changes `go.mod` and `go.sum`, in one commit per group that passed, with a source change only where a major required it and the file named in the description. The `go` directive is unchanged unless the description says which module moved it.

The description is one line per bump with old and new versions, the indirect modules that moved with them, a list of upgrades left out with the compile error, test failure or vulnerability report that excluded them, and the result lines of `go build`, `go vet`, `go test -race`, `govulncheck` and `golangci-lint run`. The trajectory keeps the full output for every group, including the reverted ones, and the rendered README.

Before merging, a reviewer checks that the four Charm v2 libraries moved together, that the `ultraviolet` and old `lipgloss` pseudo-versions in the indirect block landed on versions the Charm release expects, and that a left-out bump is not the one `govulncheck` wanted.

## Run it on every Monday morning

Create an automation in the Project with this prompt and a `schedule` trigger (`cron` `0 9 * * 1` in your `timezone`). Each week the run opens one pull request with that week's bumps, or none when nothing moved. Because each group is its own commit, dropping a bump before merge is a revert of one commit.

To have the link reach your server, start the weekly run from your own scheduler instead, with `POST /o/{org}/sessions` and an API key that has a webhook URL: Rush then POSTs `run.completed` with the `session_url` when the turn ends, and a handler on your side can post the pull request link where the maintainers read. A run that ends with no safe group opens no pull request and says why in its result.

## Why run it on Rush?

A grouped bot pull request on a Go CLI is all or nothing: one Bubble Tea release that changes a view API turns the whole month red and the other bumps wait with it. On Rush the agent tries the groups one at a time in a sandbox, builds and tests with the race detector after each, reverts the one that fails, and keeps the output, so the pull request that reaches you carries everything that passed and names the one that did not.

Running `govulncheck` and a real render of the binary after every group is the kind of check a person skips when the suite is green. The record shows each run, and the same request runs on every Go tool you maintain at the same time.

## Frequently asked questions

### Dependabot already groups gomod updates here. Why this?

Dependabot opens the group and CI reports red or green for the whole thing. This run splits the group by module family, keeps the parts that pass, reverts the part that fails, and explains why in the description, so a single breaking release does not block the rest for a month.

### What about the pseudo-versions in the indirect block?

They are reported, not chased. When the Charm group moves, `go mod tidy` resolves `ultraviolet` and the old `lipgloss` to whatever the new releases require, and the pull request lists the before and after. The agent does not pin them by hand.

### Will it move the go directive?

Only if a module in a passing group requires a newer Go, and then the description names the module. Otherwise `go 1.26.7` stays and the toolchain the sandbox uses matches it.

### How does it check the TUI still works?

The `ui/` tests run under the race detector, and the rendering path is exercised by piping a README through the built binary with `--style notty`. Interactive behavior in the Bubble Tea program is not covered by that and the pull request says so; a UI change in a Charm release is a reason to try the binary by hand before merging.

### Can I exclude a module?

Yes. Name it in the prompt or add it to the ignore list in `.github/dependabot.yml`, which the agent reads and respects. It stays at its current version with a line in the pull request saying so.
