Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "godebugdiff-marketplace",
"description": "Marketplace for godebugdiff, which lists the GODEBUG defaults that change when you raise the go line in go.mod.",
"description": "Marketplace for godebugdiff, which lists the GODEBUG defaults that change when you raise the go line in go.mod or go.work.",
"owner": {
"name": "Giulliano Macedo"
},
"plugins": [
{
"name": "godebugdiff",
"source": "./plugin",
"description": "List the GODEBUG behavior defaults that change when you raise the go line in go.mod, and which binaries they reach."
"description": "List the GODEBUG behavior defaults that change when you raise the go line in go.mod or go.work, and which binaries they reach."
}
]
}
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
Claude Code skill and Go CLI that lists the GODEBUG defaults that change when the go line in go.mod is raised, and which binaries import the affected packages.

## Architecture
`plugin/scripts/godebugdiff.go` holds all the logic in one standard-library file, so `go run` needs no setup. `Diff` copies go.mod and go.sum to a temporary directory for each version, `listAt` runs `go list -modfile -json` offline on each copy, `compare` diffs each binary's `DefaultGODEBUG` and checks its imports, `loadTable` and `loadDocs` read package names and descriptions from `$GOROOT`, and `Print` writes the report. `plugin/skills/checking-go-version-bumps/SKILL.md` tells Claude how to use it. See `docs/architecture.md`.
`plugin/scripts/godebugdiff.go` holds all the logic in one standard-library file, so `go run` needs no setup. `Diff` copies go.mod and go.sum (or go.work and go.work.sum with `--work`) to a temporary directory for each version, `listAt`/`listAtWork` run `go list` offline on each copy, `compare` diffs each binary's `DefaultGODEBUG` and checks its imports, `loadTable` and `loadDocs` read package names and descriptions from `$GOROOT`, and `Print` writes the report. `plugin/skills/checking-go-version-bumps/SKILL.md` tells Claude how to use it. See `docs/architecture.md`.

## Commands
- `make test`: gofmt, vet, unit tests and integration tests in `tests/`
- `make lint`: plugin and marketplace validation
- `make demo`: run on the example modules

## Conventions
- Standard library only. The tool runs only `go env`, `go mod edit -json` to read go.mod, `go mod edit -go` on its temporary copies, and `go list`, always with `GOPROXY=off` and `GOTOOLCHAIN=local`. Never edit the user's go.mod, never build or run their code, never use the network.
- Standard library only. The tool runs only `go env`, `go mod edit -json` to read go.mod, `go work edit` to read and copy go.work, `go mod edit -go`/`go work edit -go` on its temporary copies, and `go list`, always with `GOPROXY=off` and `GOTOOLCHAIN=local`. Never edit the user's go.mod or go.work, never build or run their code, never use the network.
- The toolchain is the source of truth. Don't add a hand-written table of settings: read them from `go list` and `$GOROOT`.
- README examples are real output from the example modules. Regenerate them after changing the output format.

Expand Down
39 changes: 28 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# godebugdiff: see what changes before you bump the go line in go.mod
# godebugdiff: see what changes before you bump the go line in go.mod or go.work

[![CI](https://github.com/GillMacedo/godebugdiff/actions/workflows/ci.yml/badge.svg)](https://github.com/GillMacedo/godebugdiff/actions/workflows/ci.yml) [![MIT license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)

**A Claude Code skill and Go CLI for Go developers about to raise the `go` directive in go.mod, for example from `go 1.21` to `go 1.24`. It lists the GODEBUG defaults that change, which of your binaries import the package each setting controls, and the `godebug` lines that keep the old behavior. The answers come from your installed Go toolchain, offline, without editing go.mod.**
**A Claude Code skill and Go CLI for Go developers about to raise the `go` directive in go.mod, or the go line in go.work, for example from `go 1.21` to `go 1.24`. It lists the GODEBUG defaults that change, which of your binaries import the package each setting controls, and the `godebug` lines or `//go:debug` directives that keep the old behavior. The answers come from your installed Go toolchain, offline, without editing go.mod or go.work.**

Since Go 1.21, the `go` line in your main module's go.mod sets the GODEBUG defaults your binaries are built with. Raising it switches `net/http` routing, TLS key exchange, x509 parsing, `math/rand.Seed`, GOMAXPROCS in containers and more to the new behavior, with no code change and no compile error. The list is spread across many sections of the [GODEBUG history](https://go.dev/doc/godebug#history). `godebugdiff` asks the toolchain itself for each binary's defaults at both go lines and shows the difference. The skill then has Claude check whether your code uses each changed API.
Since Go 1.21, the `go` line in your main module's go.mod (or in go.work, in workspace mode) sets the GODEBUG defaults your binaries are built with. Raising it switches `net/http` routing, TLS key exchange, x509 parsing, `math/rand.Seed`, GOMAXPROCS in containers and more to the new behavior, with no code change and no compile error. The list is spread across many sections of the [GODEBUG history](https://go.dev/doc/godebug#history). `godebugdiff` asks the toolchain itself for each binary's defaults at both go lines and shows the difference. The skill then has Claude check whether your code uses each changed API.

## What you can ask Claude

- "I want to bump go.mod from go 1.21 to 1.24. What behavior changes?"
- "We use a go.work workspace. What changes if we raise its go line?"
- "This Dependabot PR raises the go directive. Is anything going to break?"
- "After upgrading Go, our ServeMux routes return 404. Why?"
- "Which godebug lines do I need to keep the old TLS behavior after the bump?"
Expand Down Expand Up @@ -74,35 +75,50 @@ A module whose go.mod already pins its defaults with `godebug default=go1.21` st

Note: go.mod already pins default=go1.21. Pinned settings don't change with the go line, so they aren't listed.

A two-module workspace reads the go line from go.work with `--work`:

$ go run ./plugin/scripts --work --to 1.24 examples/workspace
go 1.21 -> 1.24 in workspace (go1.27.3, 2 binaries)

go.work has no `godebug` lines, so in workspace mode the report lists `//go:debug` directives for each main package instead of a go.mod block.

On a real project: [age](https://github.com/FiloSottile/age) at commit `b74dce4`, from its `go 1.25.0` to 1.27, has 8 binaries. 4 of the 6 changed defaults reach one of them, and `x509sslcertoverrideplatform` and `urlstrictcolons` reach only `cmd/age`. Output from five open-source projects is in [`examples/real/`](examples/real/).

## Usage

godebugdiff [--from VERSION] [--to VERSION] [--tests] [--json] [--fail] [DIR]
godebugdiff [--from VERSION] [--to VERSION] [--tests] [--work] [--json] [--fail] [DIR]

- `DIR` is the module root, the directory with go.mod. It defaults to `.`.
- `DIR` is the module root (the directory with go.mod) or, with `--work`, the workspace root (the directory with go.work). It defaults to `.`.
- `--to` defaults to the installed toolchain's version, such as 1.27 for go1.27.3. It can't be newer than the toolchain, because older toolchains don't know about later settings.
- `--from` defaults to the go line in go.mod. Pass the base branch's version to review a change that already raised it.
- `--from` defaults to the go line in go.mod, or in go.work with `--work`. Pass the base branch's version to review a change that already raised it.
- `--tests` adds test binaries. A module with no main package, such as a library, is checked through its test binaries automatically.
- `--work` reads the go line from `DIR/go.work` instead of `DIR/go.mod`, and checks every module in the workspace.
- `--json` prints the full result. `--fail` exits 1 when a change reaches a package one of your binaries imports. Exit code 2 means bad usage, or the go command couldn't load the module.

In CI, on a pull request that changes go.mod:

base=$(git show origin/main:go.mod | awk '$1 == "go" { print $2 }')
go run github.com/GillMacedo/godebugdiff/plugin/scripts@latest --from "$base" .

For a workspace, read the base go line from go.work and pass `--work`:

base=$(git show origin/main:go.work | awk '$1 == "go" { print $2 }')
go run github.com/GillMacedo/godebugdiff/plugin/scripts@latest --work --from "$base" .

## How it works

It copies go.mod and go.sum to a temporary directory twice, sets the go line with `go mod edit -go`, and runs `go list -modfile` on each copy with `GOPROXY=off` and `GOTOOLCHAIN=local`. `go list` reports each main package's `DefaultGODEBUG` and its imports, so the result comes from the same code that sets defaults in `go build`. Package names and versions come from the toolchain's own table in `$GOROOT/src/internal/godebugs`, and the one-line descriptions from `$GOROOT/doc/godebug.md`. Existing `godebug` lines in go.mod and `//go:debug` directives are applied by the toolchain, so pinned settings don't show up.

With `--work`, it copies go.work and go.work.sum, sets the go line with `go work edit -go`, and rewrites relative `use` and local `replace` paths to absolute paths in the copy. It then runs `go list` with `GOWORK` pointing at the copy and checks every workspace module. The go line in go.work sets the defaults; `godebug` lines in module go.mod files are ignored in workspace mode.

## Limits

- **Imported isn't the same as used.** A binary that imports `net/http` gets the `httpmuxgo121` change listed even if it never creates a ServeMux. The skill has Claude search the code for the API each setting names.
- **Only GODEBUG defaults.** Other changes between Go releases, in the release notes, aren't covered. The language change to per-iteration loop variables in Go 1.22 is noted when you cross it, but not analyzed.
- **Only what your toolchain knows.** Settings removed from the toolchain no longer appear, and their new behavior already applies whatever your go line says. Use the newest toolchain you plan to build with.
- **Needs the module cache or a vendor directory.** It never downloads. If modules are missing it stops and says to run `go mod download`.
- **Crossing Go 1.17 adds requirements.** If the new go line needs `go mod tidy` changes, it makes them in the temporary copy and says so. Your go.mod isn't touched.
- **go.work is ignored.** In workspace mode the go line in go.work sets the defaults. The output says so when a go.work file is active.
- **Crossing Go 1.17 adds requirements.** In module mode, if the new go line needs `go mod tidy` changes, it makes them in the temporary copy and says so. In workspace mode it stops and asks you to run `go mod tidy`; it never edits module files.
- **Workspace files need `--work`.** Without it, an active go.work is only noted. With `--work`, the go line in go.work sets the defaults and `godebug` lines in module go.mod files are ignored.
- **Descriptions need `$GOROOT/doc/godebug.md`.** Toolchains that the go command downloads by itself, through `GOTOOLCHAIN`, don't include `doc/`, so they show only the link for each setting.
- Descriptions are the first sentence of the matching paragraph in Go's own docs, and some settings were backported, so a description can name a later release than the one in the "Go 1.N" column. The link goes to the full text.

Expand All @@ -112,24 +128,25 @@ It copies go.mod and go.sum to a temporary directory twice, sets the go line wit

**Is it safe to upgrade the Go toolchain without changing go.mod?** Mostly. With the same go line, a newer toolchain keeps the old defaults for settings it still has. Settings that were removed are the exception. `godebugdiff` covers the go line change.

**How do I keep the old behavior after raising the go line?** Add a `godebug` block to go.mod with the old values. `godebugdiff` prints that block for every change that reaches a binary. These settings are removed in later Go releases, so treat pins as temporary.
**How do I keep the old behavior after raising the go line?** Add a `godebug` block to go.mod with the old values. In a workspace, add `//go:debug` directives to each main package or set `GODEBUG` at run time; go.work has no `godebug` lines. `godebugdiff` prints the right form for every change that reaches a binary. These settings are removed in later Go releases, so treat pins as temporary.

**Why did my ServeMux routes start returning 404, or start matching differently, after a Go upgrade?** `httpmuxgo121` changed in Go 1.22. With the old value, patterns like `GET /items/{id}` are literal paths. See `examples/muxcheck`.

**Does my library's go line change GODEBUG for programs that use it?** No. Only the main module's go line counts. Raising a library's go line does force those programs to need that Go version, which raises their go line too.
**Does my library's go line change GODEBUG for programs that use it?** No. Only the main module's go line (or the go.work go line in workspace mode) counts. Raising a library's go line does force those programs to need that Go version, which raises their go line too.

**Is it an MCP server?** No. It's a skill with one Go source file, so `go run` is all it needs.

## Tested on

- **Example modules in `examples/`** with Go 1.27.3 on Linux arm64. The integration tests in `tests/` check that the settings listed for `examples/shop` match the `DefaultGODEBUG` that `go build` records in each binary (read with `go version -m`), at both go lines.
- **A real behavior change:** `examples/muxcheck` built at go 1.21 returns 404 and at go 1.22 returns 200, and `godebugdiff` lists `httpmuxgo121` for it.
- **A workspace:** `examples/workspace` has two modules and a go.work with relative `use` paths. The integration test checks both binaries and that go.work is unchanged.
- **Healthy cases stay quiet:** `examples/pinned` with `godebug default=go1.21`, and any module compared at the same version, print "No GODEBUG defaults change."
- **Real projects**, to the installed 1.27: hey `5626f79` (go 1.24.0), fzf `26ff936` (go 1.23.0), age `b74dce4` (go 1.25.0, 8 binaries), cobra `adbc881` (a library on go 1.15, which needed the temporary tidy) and rclone `f0cb5dd` (which pins two settings in go.mod). Outputs are in `examples/real/`. A vendored copy of hey gave the same result with an empty module cache, and hey without a vendor directory and with an empty cache stopped with the "run go mod download" message. None of these checked whether the projects use each changed API.
- **Go 1.22.12**, a downloaded toolchain: the unit tests and the ServeMux test pass, and the tests that need a newer toolchain skip. On `examples/muxcheck` it also lists `tls10server`, `tlsrsakex` and `tlsunsafeekm`, which Go 1.27 removed, and shows links without descriptions.
- **Errors:** a `godebug` line with a removed setting, `--to` newer than the toolchain, and a directory with no go.mod.

It hasn't been tested on Windows or macOS, on toolchains other than 1.22.12 and 1.27.3, with go.work workspaces beyond the note, in a fresh Claude session, or through `go run ...@latest`, which needs the published repo.
It hasn't been tested on Windows, on toolchains other than 1.22.12 and 1.27.3, in a fresh Claude session, or through `go run ...@latest`, which needs the published repo.

## Contributing

Expand Down
1 change: 0 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

## Next
- A Markdown output for pull request comments
- Read the go line from go.work in workspace mode, instead of only noting it
- Show which `godebug` pins in go.mod are already the default at the new version, so they can be removed

## Later
Expand Down
10 changes: 9 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
go.mod --> two temporary copies, with the go line set by `go mod edit -go`
each copy --> `go list -e -modfile=COPY -json ./...` with GOPROXY=off, GOTOOLCHAIN=local, GOWORK=off
--> main packages (or test binaries) with DefaultGODEBUG and Deps

go.work (--work) --> two temporary copies, with the go line set by `go work edit -go`
--> relative `use` and local `replace` paths rewritten to absolute
--> `go list -m -json` to find the workspace modules
--> `go list -e -json MODULE/...` for each module with GOWORK=COPY
--> main packages (or test binaries) with DefaultGODEBUG and Deps

compare --> settings whose value differs, per binary --> one Change per setting, with the binaries that import its package
$GOROOT/src/internal/godebugs/table.go --> package and version for each setting
$GOROOT/doc/godebug.md --> first sentence of the paragraph that names the setting, and a link to its section
Expand All @@ -11,7 +18,8 @@
- **Source of truth:** `go list` computes `DefaultGODEBUG` with the same code `go build` uses. It applies the go line, `godebug` lines in go.mod and `//go:debug` directives. The integration tests check the result against `go version -m` on built binaries.
- **Reach:** a setting reaches a binary when its package, from the toolchain's table, is in the binary's `Deps`. Settings whose package starts with `cmd/` control the go command and are listed separately.
- **Libraries:** a module with no main package is checked through `go list -test`, which gives each test binary's defaults.
- **Errors:** if a binary's imports don't load, results would be incomplete, so the tool stops with exit code 2. If the new go line needs `go mod tidy` changes, which happens when crossing Go 1.17, it retries on a fresh copy with `-mod=mod`, using only the module cache.
- **Errors:** if a binary's imports don't load, results would be incomplete, so the tool stops with exit code 2. If the new go line needs `go mod tidy` changes, which happens when crossing Go 1.17, it retries on a fresh copy with `-mod=mod`, using only the module cache. In workspace mode it stops and asks for `go mod tidy` instead, since it must not edit module files.
- **Workspaces:** `--work` copies go.work and go.work.sum, rewrites relative `use` and local `replace` paths to absolute paths, and sets `GOWORK` to the copy. It lists each workspace module by import path because `./...` from the workspace root matches no module.
- **Reading GOROOT:** `table.go` is an internal file and could change shape. If it can't be parsed, changes are still listed, without package names, so they can't be marked as reaching a binary.

Key choices: one standard-library Go file run with `go run`, and the toolchain as the only data source. See `decisions/`.
9 changes: 9 additions & 0 deletions evals/evals.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,14 @@
"Explains that only GODEBUG defaults are covered",
"Doesn't claim the upgrade is safe"
]
},
{
"skills": ["checking-go-version-bumps"],
"query": "We use a go.work workspace. I want to raise its go line from 1.21 to 1.24. What changes?",
"expected_behavior": [
"Runs godebugdiff.go with --work --to 1.24 on the workspace root",
"Reads the go line from go.work, not a module go.mod",
"Says that workspace pins use //go:debug or GODEBUG because go.work has no godebug lines"
]
}
]
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All modules here are made up and use only the standard library.
- `shop/`: go 1.21, two binaries. `cmd/api` serves HTTP and `cmd/worker` only imports `fmt` and `math/rand`, so most changes reach only the API. `shop-1.21-to-1.24.txt` is its real output, from `make examples`.
- `pinned/`: go 1.21 with `godebug default=go1.21`, so raising the go line changes no defaults. This is the healthy case.
- `ledgerlib/`: a library with no main package, checked through its test binary.
- `workspace/`: a two-module workspace on go 1.21. `app` serves HTTP and `worker` imports `math/rand`; `go.work` uses relative paths. `godebugdiff --work --to 1.24 examples/workspace` reads the go line from `go.work`.

## real/

Expand Down
3 changes: 3 additions & 0 deletions examples/workspace/app/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module example.com/workspace/app

go 1.21
7 changes: 7 additions & 0 deletions examples/workspace/app/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

import "net/http"

func main() {
_ = http.NewServeMux()
}
6 changes: 6 additions & 0 deletions examples/workspace/go.work
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
go 1.21

use (
./app
./worker
)
3 changes: 3 additions & 0 deletions examples/workspace/worker/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module example.com/workspace/worker

go 1.21
10 changes: 10 additions & 0 deletions examples/workspace/worker/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

import (
"fmt"
"math/rand"
)

func main() {
fmt.Println(rand.Int())
}
Loading