Files
headscale/tools/bump/flake.go
T
Kristoffer Dalby c3d771e23e tools/bump: add the version bump tool
Keeps the interlocked pins current and reports what it could not move.
Areas apply, gate and commit one at a time, so a dependency that breaks
the build costs one commit rather than the whole pull request.
2026-09-25 15:52:26 +02:00

148 lines
3.4 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
)
// lockNode is the slice of a flake.lock entry that identifies what an input is
// pinned to.
type lockNode struct {
Locked struct {
Rev string `json:"rev"`
LastModified int64 `json:"lastModified"`
} `json:"locked"`
}
type flakeLock struct {
Nodes map[string]lockNode `json:"nodes"`
}
func readLock(r *repo) (flakeLock, error) {
var lock flakeLock
b, err := r.readFile("flake.lock")
if err != nil {
return lock, err
}
if err := json.Unmarshal([]byte(b), &lock); err != nil { //nolint:noinlineerr
return lock, fmt.Errorf("parsing flake.lock: %w", err)
}
return lock, nil
}
// lockDiff reports the inputs whose revision moved: one detailed line each,
// plus the bare names for the commit subject.
func lockDiff(before, after flakeLock) ([]string, []string) {
var detail, names []string
for name, post := range after.Nodes {
pre, ok := before.Nodes[name]
if !ok || post.Locked.Rev == "" || pre.Locked.Rev == post.Locked.Rev {
continue
}
detail = append(detail, fmt.Sprintf("%s %s -> %s", name, short(pre.Locked.Rev), short(post.Locked.Rev)))
names = append(names, name)
}
sort.Strings(detail)
sort.Strings(names)
return detail, names
}
func short(rev string) string {
if len(rev) > 7 {
return rev[:7]
}
return rev
}
// applyFlake refreshes every flake input. Because flake.nix asks for
// buildGoLatestModule and go_latest, this is also how Go and every devShell
// tool get a new version.
func applyFlake(ctx context.Context, r *repo) (change, error) {
before, err := readLock(r)
if err != nil {
return change{}, err
}
if _, err := r.run(ctx, "nix", "flake", "update"); err != nil { //nolint:noinlineerr
return change{}, err
}
after, err := readLock(r)
if err != nil {
return change{}, err
}
moved, names := lockDiff(before, after)
if len(moved) == 0 {
return change{Empty: true}, nil
}
detail := moved
if tools, err := toolVersions(ctx, r); err == nil { //nolint:noinlineerr
detail = append(detail, tools...)
}
return change{
Summary: shortList(names),
Detail: detail,
}, nil
}
// shortList keeps a commit subject readable when many inputs move at once.
func shortList(names []string) string {
const maxNamed = 3
if len(names) <= maxNamed {
return strings.Join(names, ", ")
}
return fmt.Sprintf("%s and %d more", strings.Join(names[:maxNamed], ", "), len(names)-maxNamed)
}
// gateFlake proves the flake still evaluates before anything expensive runs.
func gateFlake(ctx context.Context, r *repo) error {
_, err := r.run(ctx, "nix", "eval", "--raw",
fmt.Sprintf(".#packages.%s.headscale.name", r.System))
return err
}
// goVersion is the Go the devShell now provides, without the "go" prefix.
func goVersion(ctx context.Context, r *repo) (string, error) {
out, err := r.nixRun(ctx, "go", "env", "GOVERSION")
if err != nil {
return "", err
}
return strings.TrimPrefix(strings.TrimSpace(out), "go"), nil
}
// toolVersions reports the versions that most often decide whether a lock bump
// turns the pull request red.
func toolVersions(ctx context.Context, r *repo) ([]string, error) {
goVer, err := goVersion(ctx, r)
if err != nil {
return nil, err
}
versions := []string{"go " + goVer}
if out, err := r.nixRun(ctx, "golangci-lint", "version"); err == nil { //nolint:noinlineerr
versions = append(versions, strings.TrimSpace(strings.SplitN(out, "\n", 2)[0]))
}
return versions, nil
}