tools/bump: bisect go.mod one module at a time

golang.org/x/net deprecated http2.Server, staticcheck failed on
hscontrol/noise.go, and the whole batch of 50 direct requirements was
dropped for it. The bisect tries every atom together first, so one atom
per module costs nothing until something actually breaks.
This commit is contained in:
Kristoffer Dalby
2026-09-09 13:27:51 +00:00
parent 2cf100360d
commit 8ed39eeb0c
2 changed files with 57 additions and 18 deletions
+5 -1
View File
@@ -76,7 +76,11 @@ func applySet(ctx context.Context, r *repo, atoms []atom) ([]string, error) {
return nil, fmt.Errorf("%s: %w", a.Name, err)
}
summaries = append(summaries, summary)
// An atom that found nothing to do reports no summary; listing it
// would fill the report with modules that did not move.
if summary != "" {
summaries = append(summaries, summary)
}
}
err := settle(ctx, r)
+52 -17
View File
@@ -165,12 +165,12 @@ func sqliteAtom(ctx context.Context, r *repo) (string, error) {
return fmt.Sprintf("modernc.org/sqlite %s -> %s (libc %s)", before, latest, libc), nil
}
// restAtom upgrades every direct requirement that is not owned by a lockstep
// atom.
func restAtom(ctx context.Context, r *repo) (string, error) {
// directRequirements is every direct requirement not already owned by a
// lockstep atom.
func directRequirements(r *repo) ([]string, error) {
f, err := parseGoMod(r)
if err != nil {
return "", err
return nil, err
}
owned := map[string]bool{
@@ -191,23 +191,53 @@ func restAtom(ctx context.Context, r *repo) (string, error) {
paths = append(paths, req.Mod.Path)
}
if len(paths) == 0 {
return "no direct requirements", nil
}
if _, err := r.nixRun(ctx, append([]string{"go", "get", "-u"}, paths...)...); err != nil { //nolint:noinlineerr
return "", err
}
return fmt.Sprintf("%d direct requirements", len(paths)), nil
return paths, nil
}
func goModAtoms() []atom {
return []atom{
// moduleAtom upgrades one direct requirement. One atom per module costs nothing
// on a good day, because the bisect tries every atom together first and only
// splits when that fails. It is what stops a single dependency deprecating an
// API the tree still uses from taking every other upgrade down with it.
func moduleAtom(path string) func(context.Context, *repo) (string, error) {
return func(ctx context.Context, r *repo) (string, error) {
before, err := moduleVersion(ctx, r, path)
if err != nil {
return "", err
}
if _, err := r.nixRun(ctx, "go", "get", "-u", path); err != nil { //nolint:noinlineerr
return "", err
}
after, err := moduleVersion(ctx, r, path)
if err != nil {
return "", err
}
if before == after {
return "", nil
}
return fmt.Sprintf("%s %s -> %s", path, before, after), nil
}
}
func goModAtoms(r *repo) ([]atom, error) {
atoms := []atom{
{Name: "tailscale", Apply: tailscaleAtom},
{Name: "sqlite", Apply: sqliteAtom},
{Name: "rest", Apply: restAtom},
}
paths, err := directRequirements(r)
if err != nil {
return nil, err
}
for _, path := range paths {
atoms = append(atoms, atom{Name: path, Apply: moduleAtom(path)})
}
return atoms, nil
}
// lockstepPairs are the indirect dependencies whose version is dictated by
@@ -426,7 +456,12 @@ func atomGate(ctx context.Context, r *repo) error {
// flake.nix reads. Skipping that refresh is the classic way to hand over a
// pull request that cannot nix build.
func applyGoMod(ctx context.Context, r *repo) (change, error) {
kept, drops, err := applyAtoms(ctx, r, goModAtoms())
atoms, err := goModAtoms(r)
if err != nil {
return change{}, err
}
kept, drops, err := applyAtoms(ctx, r, atoms)
if err != nil {
return change{}, err
}