Update Go Dependencies
Audit and update Go module dependencies safely, one semver tier at a time. Use the native go toolchain plus govulncheck — no third-party wrappers.
Step 1: Preflight
go version
Locate go.mod:
find . -name go.mod -not -path '*/vendor/*' -not -path '*/.git/*' -not -path '*/node_modules/*'
If multiple go.mod files are found (nested modules), ask the user which module to target, or whether to iterate all.
Check for go.work:
test -f go.work && cat go.work
If a workspace exists, ask whether to target a single module or iterate across the workspace.
Step 2: Detect govulncheck
command -v govulncheck
If missing, inform the user and ask for approval to install:
go install golang.org/x/vuln/cmd/govulncheck@latest
If declined, proceed without the vuln scan — note it in the final report.
Step 3: List outdated modules
go list -u -m -json all
This streams one JSON object per module (not a single array). Parse by splitting on }\n{ or using a JSON stream parser. Relevant fields:
{
"Path": "github.com/stretchr/testify",
"Version": "v1.8.0",
"Update": { "Path": "github.com/stretchr/testify", "Version": "v1.9.0" },
"Main": false,
"Indirect": false,
"Replace": null
}
Default to direct dependencies only (Indirect: false, Main: false). go mod tidy drags indirects along automatically after direct bumps. Only include indirects if the user explicitly asks.
Skip modules with a non-nil Replace — they point to a local path or forked repo and must be updated manually. Report them separately.
Step 4: Run govulncheck (if available)
govulncheck ./... -format json
Parse the stream. Each finding event with osv populated is an advisory. Record:
{ module, vulnerable_ranges, fixed_in, severity, summary }
Map fixed_in to the outdated list so you know which advisories each tier's updates would resolve.
Step 5: Categorize by semver
For each module with an Update, classify the jump from Version to Update.Version:
- Patch:
v1.2.3→v1.2.5 - Minor:
v1.2.3→v1.3.0 - Major:
v1.2.3→v2.0.0
Additional Go-specific rules:
+incompatiblesuffix (e.g.,v2.3.0+incompatible): legacy pre-modules v2+. Treat as major and flag — these modules never adopted Go modules properly.- v2+ module path bump: a module currently at
github.com/foo/barwithlatest = v2.x.xrequires the import path to change togithub.com/foo/bar/v2(Go modules convention SIV — semantic import versioning). Detect by comparinglatestmajor ≥ 2 and current path has no/vNsuffix. Handle in the major tier with special rewriting flow. - Pre-1.0 (v0.y.z):
v0.2.3→v0.2.9= patch;v0.2.3→v0.3.0= major (not minor). Go pre-1.0 libraries make breaking changes between v0.y bumps. gopkg.inmodules: versioning lives in the import path (gopkg.in/yaml.v2vsgopkg.in/yaml.v3). A jump fromv2tov3requires a manual import path change, not ago get.
Step 6: Fetch release notes (minor + major only)
Skip patches. For each minor and major update:
ghfor GitHub-hosted modules (most common):gh release view v<X.Y.Z> -R <owner>/<repo>The module path
github.com/<owner>/<repo>maps directly to the GitHub repo. For modules in subdirectories (e.g.,github.com/foo/bar/subpkg), the release is on the repo root, sometimes taggedsubpkg/v<X.Y.Z>.Context7 MCP for well-known libraries (less coverage than npm, but fast when present):
mcp__context7__resolve-library-idwith the module pathmcp__context7__query-docswithquery="changelog"orquery="migration"
WebFetch fallback:
https://pkg.go.dev/<module>?tab=versions— shows version listhttps://pkg.go.dev/<module>— may link to changelog
Summarize each into one line with breaking changes flagged.
Step 7: Apply updates in tiers
Apply in strict order: patch → minor → major. Run verification (Step 8) after each tier and after each individual major. On failure, offer rollback and stop.
Tier 1 — Patches (auto-apply)
For each patch in the batch:
go get <module>@<patch-version>
Then tidy once:
go mod tidy
If a vendor/ directory with modules.txt exists, also run:
go mod vendor
Report count after.
Tier 2 — Minor (approval gate)
Present the full minor batch as a table (Module | Current → Latest | Fixes advisory? | Release notes). Ask the user: approve the batch, deselect specific modules, or skip tier. Apply each approved module with go get, then go mod tidy once at the end.
Tier 3 — Major (per-module confirmation)
Iterate majors one at a time. For each, first classify:
Case A — same module path (module already at /vN or major is still v0/v1):
- Ask per-module: apply / skip / defer.
- If apply:
go get <module>@v<N>.0.0, thengo mod tidy, then verify.
Case B — v2+ path change required (current path has no /vN, new major ≥ 2):
- Show a LOUD warning: "This bump changes the import path from
github.com/foo/bartogithub.com/foo/bar/v2. Everyimportstatement referencing this module in your code must be rewritten." - Count affected files:
grep -rl "\"<current-module-path>\"" --include='*.go' . - Show the file count and ask: apply (rewrite imports +
go get) / skip / defer. - If apply:
- Update
go.mod:go get <new-module-path>@v<N>.0.0(this adds the/vNpath as a new dependency). - Rewrite imports in source. Prefer
gofmt -rfor mechanical rewrites:
(Note:gofmt -w -r '"<old-path>" -> "<new-path>"' .gofmt -roperates on expressions, not strings. For import-path rewrites specifically, a sed-based approach is more reliable — or usegoimportsafter manual sed:)grep -rl "\"<old-path>\"" --include='*.go' . | xargs sed -i.bak "s|\"<old-path>\"|\"<new-path>\"|g" find . -name '*.bak' -delete go mod tidy— this should drop the old path fromgo.mod.- Verify.
- Update
gopkg.inspecial case: path changes likegopkg.in/yaml.v2→gopkg.in/yaml.v3follow the same rewrite flow — the "major" is encoded in the path segment.
Step 8: Verify
After each tier (and after each individual major):
go build ./...
go vet ./...
Offer (don't force — can be slow):
go test ./...
On failure:
- Report the first 50 lines of error output.
- Offer rollback:
git checkout -- go.mod go.sumand any source files changed for import rewrites. Thengo mod tidyto re-sync. - Do not proceed to the next tier.
Step 9: Final report
# Go Dependencies Update Report
**Module:** github.com/example/app
**Go version:** go1.24
## Applied
- patch: 8 modules (1 fixed advisory)
- minor: 3 modules
- major: 1 module (github.com/foo/bar → /v2, 14 imports rewritten)
## Skipped / deferred
- `github.com/old/lib` — has `replace` directive, manual update needed
- `github.com/big/lib` → v3 — deferred, requires app-wide migration
## Vulnerabilities (govulncheck)
- Resolved: 2 (GO-2024-XXXX critical, GO-2024-YYYY high)
- Remaining: 0
## Verification
- go build ./...: ok
- go vet ./...: ok
- go test ./...: not run (user declined)
Guardrails
- Never edit
go.sumby hand. Usego get/go mod tidyexclusively. - Never bypass git hooks. Never commit or push.
- Skip
replacedirectives — flag them in the report; user handles manually. - Respect
toolchaindirective ingo.mod: if an update requires a newer Go toolchain than the project'stoolchainline, flag and skip. vendor/mode: ifvendor/modules.txtexists, rungo mod vendoraftergo mod tidyso the vendor tree stays in sync.GOFLAGS=-mod=vendor: detect viago env GOFLAGS; updates still work but the user must re-vendor.- GOPRIVATE / GOPROXY: if the project uses a private proxy,
go list -uandgo getrespect those env vars — no action needed, just noted.