Files
headscale/tools/bump/modproxy.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

137 lines
3.7 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
)
// proxyBase is the module mirror. It is deliberately the same source the go
// command already trusts: immutable per version, so the go.mod read back here
// is the one that will actually be resolved, not whatever a branch tip happens
// to hold at fetch time.
const proxyBase = "https://proxy.golang.org"
const proxyTimeout = 30 * time.Second
// errNoLockstepSource means the partner version could not be established. The
// caller must move neither half of the pair: a libc without its sqlite is the
// exact breakage the lockstep rule exists to prevent.
var errNoLockstepSource = errors.New("lockstep source unavailable")
// fetch performs a GET and returns the body, or an error naming the status.
func fetch(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("building request for %s: %w", url, err)
}
client := &http.Client{Timeout: proxyTimeout}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("GET %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET %s: %w: %s", url, errHTTPStatus, resp.Status)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", url, err)
}
return b, nil
}
var errHTTPStatus = errors.New("unexpected status")
// latestVersion resolves a module's newest version through the proxy.
func latestVersion(ctx context.Context, path string) (string, error) {
esc, err := module.EscapePath(path)
if err != nil {
return "", fmt.Errorf("escaping %s: %w", path, err)
}
body, err := fetch(ctx, proxyBase+"/"+esc+"/@latest")
if err != nil {
return "", err
}
var info struct {
Version string `json:"Version"`
}
if err := json.Unmarshal(body, &info); err != nil { //nolint:noinlineerr
return "", fmt.Errorf("decoding @latest for %s: %w", path, err)
}
if info.Version == "" {
return "", fmt.Errorf("%w: empty @latest for %s", errNoLockstepSource, path)
}
return info.Version, nil
}
// modFileOf fetches and parses the go.mod of the exact path@version.
func modFileOf(ctx context.Context, path, version string) (*modfile.File, error) {
escPath, err := module.EscapePath(path)
if err != nil {
return nil, fmt.Errorf("escaping %s: %w", path, err)
}
escVer, err := module.EscapeVersion(version)
if err != nil {
return nil, fmt.Errorf("escaping version %s: %w", version, err)
}
body, err := fetch(ctx, proxyBase+"/"+escPath+"/@v/"+escVer+".mod")
if err != nil {
return nil, err
}
f, err := modfile.Parse(path+"@"+version+"/go.mod", body, nil)
if err != nil {
return nil, fmt.Errorf("parsing go.mod of %s@%s: %w", path, version, err)
}
return f, nil
}
// requiredVersion reports the version of dep that f requires.
func requiredVersion(f *modfile.File, dep string) (string, bool) {
for _, req := range f.Require {
if req.Mod.Path == dep {
return req.Mod.Version, true
}
}
return "", false
}
// partnerVersion resolves the version of dep that owner@ownerVersion pins. It
// is the whole of the lockstep rule: the partner is never guessed, only read
// off the owner's own go.mod.
func partnerVersion(ctx context.Context, owner, ownerVersion, dep string) (string, error) {
f, err := modFileOf(ctx, owner, ownerVersion)
if err != nil {
return "", fmt.Errorf("%w: %w", errNoLockstepSource, err)
}
v, ok := requiredVersion(f, dep)
if !ok {
return "", fmt.Errorf("%w: %s@%s does not require %s", errNoLockstepSource, owner, ownerVersion, dep)
}
return v, nil
}