Files
headscale/tools/bump/verify.go
T
Kristoffer Dalby 30d10640ea tools/bump: reword the verify failure
"invariant" is not language this repository uses.
2026-09-25 15:52:26 +02:00

154 lines
3.6 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"log"
"strings"
"golang.org/x/mod/semver"
)
var errPinsDisagree = errors.New("version pins disagree")
// finding is one disagreement, phrased so the fix is obvious.
type finding string
// cmdVerify checks that the pins agree with each other and with their
// upstreams. It is deliberately separate from run: the same checks catch a
// hand-written commit that breaks a lockstep rule, not just a bad automated one.
func cmdVerify(ctx context.Context) error {
r, err := openRepo(ctx)
if err != nil {
return err
}
findings := make([]finding, 0, 4)
findings = append(findings, verifyLockstep(ctx, r)...)
findings = append(findings, verifyBuilders(ctx, r)...)
findings = append(findings, verifyToolchain(ctx, r)...)
findings = append(findings, verifyVendorHash(ctx, r)...)
if len(findings) == 0 {
log.Print("all version pins agree")
return nil
}
for _, f := range findings {
log.Printf("- %s", f)
}
return fmt.Errorf("%w: %d finding(s)", errPinsDisagree, len(findings))
}
func verifyLockstep(ctx context.Context, r *repo) []finding {
var findings []finding
err := checkLockstep(ctx, r)
if err != nil {
findings = append(findings, finding(err.Error()))
}
err = checkModComments(r)
if err != nil {
findings = append(findings, finding(err.Error()))
}
return findings
}
// verifyBuilders checks the floor relation, not equality: a newer builder
// compiles an older module, and only the reverse fails.
func verifyBuilders(ctx context.Context, r *repo) []finding {
var findings []finding
ourMod, err := r.readFile("go.mod")
if err != nil {
return []finding{finding(err.Error())}
}
ourGo, err := goDirective(ourMod)
if err != nil {
return []finding{finding(err.Error())}
}
tsGo, tsErr := tailscaleGo(ctx)
for _, check := range []struct {
pins []goPin
floor string
err error
}{
{tailscaleBuilders, tsGo, tsErr},
{localBuilders, ourGo, nil},
} {
if check.err != nil {
findings = append(findings, finding("could not resolve the tailscale go directive: "+check.err.Error()))
continue
}
for _, pin := range check.pins {
content, err := r.readFile(pin.File)
if err != nil {
findings = append(findings, finding(err.Error()))
continue
}
have, ok := currentGolangTag(content)
if !ok {
findings = append(findings, finding(pin.File+": no golang builder image found"))
continue
}
if semver.Compare("v"+have, "v"+check.floor) < 0 {
findings = append(findings, finding(fmt.Sprintf(
"%s: golang %s is below the %s required by %s", pin.File, have, check.floor, pin.Why)))
}
}
}
return findings
}
// verifyToolchain reports, but never edits, a go.mod directive that has fallen
// behind the toolchain the build actually uses. Raising it is a promise to
// downstream packagers, so it stays a human decision.
func verifyToolchain(ctx context.Context, r *repo) []finding {
nixGo, err := goVersion(ctx, r)
if err != nil {
return []finding{finding(err.Error())}
}
ourMod, err := r.readFile("go.mod")
if err != nil {
return []finding{finding(err.Error())}
}
ourGo, err := goDirective(ourMod)
if err != nil {
return []finding{finding(err.Error())}
}
if semver.Compare("v"+ourGo, "v"+nixGo) > 0 {
return []finding{finding(fmt.Sprintf(
"go.mod requires go %s but the devShell provides %s", ourGo, nixGo))}
}
return nil
}
func verifyVendorHash(ctx context.Context, r *repo) []finding {
out, err := r.nixRun(ctx, "go", "run", "./cmd/vendorhash", "check")
if err != nil {
return []finding{finding("flakehashes.json is stale: " + strings.TrimSpace(out))}
}
return nil
}