bc62ad02
build: the toolchain moves to go 1.27
a73x 2026-08-20 18:14
Commit message
Makefile
| Old | New | ||
|---|---|---|---|
| @@ -2,7 +2,7 @@ BIN := bin | |||
| 2 | WEB_DIST := internal/server/web/dist | 2 | WEB_DIST := internal/server/web/dist |
| 3 | 3 | ||
| 4 | # Pinned so local and CI lint identically. Bump deliberately. | 4 | # Pinned so local and CI lint identically. Bump deliberately. |
| 5 | GOLANGCI_VERSION := v2.12.2 | 5 | GOLANGCI_VERSION := v2.13.0 |
| 6 | GOLANGCI := $(shell go env GOPATH)/bin/golangci-lint | 6 | GOLANGCI := $(shell go env GOPATH)/bin/golangci-lint |
| 7 | # Pinned dead-code analyzer (golang.org/x/tools/cmd/deadcode). Bump deliberately. | 7 | # Pinned dead-code analyzer (golang.org/x/tools/cmd/deadcode). Bump deliberately. |
| 8 | DEADCODE_VERSION := v0.48.0 | 8 | DEADCODE_VERSION := v0.48.0 |
| @@ -82,15 +82,22 @@ ship: | |||
| 82 | arch: | 82 | arch: |
| 83 | go test -count=1 ./internal/arch/ | 83 | go test -count=1 ./internal/arch/ |
| 84 | 84 | ||
| 85 | $(GOLANGCI): | 85 | # `go install` always writes the same unversioned path, so a file target could |
| 86 | go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION) | 86 | # not tell a stale binary from a current one: bumping GOLANGCI_VERSION left the |
| 87 | # old linter in place, and a linter built by an older Go refuses to analyse a | ||
| 88 | # newer language version outright. Check what is actually installed instead. | ||
| 89 | .PHONY: lint-tool | ||
| 90 | lint-tool: | ||
| 91 | @$(GOLANGCI) version 2>/dev/null | \ | ||
| 92 | grep -q "version $(GOLANGCI_VERSION:v%=%) built with $(shell go env GOVERSION) " || \ | ||
| 93 | go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION) | ||
| 87 | 94 | ||
| 88 | # Block tier: fails on any finding (boundaries + correctness). See .golangci.yml. | 95 | # Block tier: fails on any finding (boundaries + correctness). See .golangci.yml. |
| 89 | lint: $(GOLANGCI) | 96 | lint: lint-tool |
| 90 | $(GOLANGCI) run ./... | 97 | $(GOLANGCI) run ./... |
| 91 | 98 | ||
| 92 | # Warn tier: complexity/style, reported but never fails the build. | 99 | # Warn tier: complexity/style, reported but never fails the build. |
| 93 | lint-extra: $(GOLANGCI) | 100 | lint-extra: lint-tool |
| 94 | $(GOLANGCI) run --default=none --enable=$(LINT_WARN) --issues-exit-code=0 ./... | 101 | $(GOLANGCI) run --default=none --enable=$(LINT_WARN) --issues-exit-code=0 ./... |
| 95 | 102 | ||
| 96 | # Per-package coverage ratchet (see scripts/coverage.sh). | 103 | # Per-package coverage ratchet (see scripts/coverage.sh). |
go.mod
| Old | New | ||
|---|---|---|---|
| @@ -1,6 +1,6 @@ | |||
| 1 | module github.com/a73x/eitri | 1 | module github.com/a73x/eitri |
| 2 | 2 | ||
| 3 | go 1.26.4 | 3 | go 1.27.0 |
| 4 | 4 | ||
| 5 | require ( | 5 | require ( |
| 6 | github.com/coder/websocket v1.8.15 | 6 | github.com/coder/websocket v1.8.15 |
internal/agent/hyperlog/hyperlog.go
| Old | New | ||
|---|---|---|---|
| @@ -17,6 +17,7 @@ import ( | |||
| 17 | "bytes" | 17 | "bytes" |
| 18 | "io" | 18 | "io" |
| 19 | "os" | 19 | "os" |
| 20 | "slices" | ||
| 20 | "strings" | 21 | "strings" |
| 21 | "unicode" | 22 | "unicode" |
| 22 | ) | 23 | ) |
| @@ -69,8 +70,8 @@ func Reason(path string) string { | |||
| 69 | } | 70 | } |
| 70 | 71 | ||
| 71 | lines := strings.Split(string(buf), "\n") | 72 | lines := strings.Split(string(buf), "\n") |
| 72 | for i := len(lines) - 1; i >= 0; i-- { | 73 | for _, line := range slices.Backward(lines) { |
| 73 | if s := clean(lines[i]); s != "" { | 74 | if s := clean(line); s != "" { |
| 74 | return s | 75 | return s |
| 75 | } | 76 | } |
| 76 | } | 77 | } |
internal/server/api/hostinfo_api_test.go
| Old | New | ||
|---|---|---|---|
| @@ -34,8 +34,8 @@ func TestToHostResponseMetricsOnlyWhenOnline(t *testing.T) { | |||
| 34 | h := store.Host{ID: "h1", Name: "host-a", OSPretty: "Debian GNU/Linux 12 (bookworm)"} | 34 | h := store.Host{ID: "h1", Name: "host-a", OSPretty: "Debian GNU/Linux 12 (bookworm)"} |
| 35 | 35 | ||
| 36 | online := registry.HostState{ | 36 | online := registry.HostState{ |
| 37 | Report: registry.Report{Metrics: registry.Metrics{UptimeS: 3600, MemUsedMB: 2048, Load1: 1.5, DiskFreeGB: 80}}, | 37 | Metrics: registry.Metrics{UptimeS: 3600, MemUsedMB: 2048, Load1: 1.5, DiskFreeGB: 80}, |
| 38 | Online: true, | 38 | Online: true, |
| 39 | } | 39 | } |
| 40 | hr := toHostResponse(h, online, true, store.Alloc{}) | 40 | hr := toHostResponse(h, online, true, store.Alloc{}) |
| 41 | require.NotNil(t, hr.Metrics) | 41 | require.NotNil(t, hr.Metrics) |
| @@ -51,8 +51,8 @@ func TestToHostResponseMetricsOnlyWhenOnline(t *testing.T) { | |||
| 51 | // Stale registry state (reported once, then went silent): ok=true but | 51 | // Stale registry state (reported once, then went silent): ok=true but |
| 52 | // Online=false. Metrics must NOT be served stale — gated on Online, not ok. | 52 | // Online=false. Metrics must NOT be served stale — gated on Online, not ok. |
| 53 | stale := registry.HostState{ | 53 | stale := registry.HostState{ |
| 54 | Report: registry.Report{Metrics: registry.Metrics{UptimeS: 3600, Load1: 1.5}}, | 54 | Metrics: registry.Metrics{UptimeS: 3600, Load1: 1.5}, |
| 55 | Online: false, | 55 | Online: false, |
| 56 | } | 56 | } |
| 57 | hr = toHostResponse(h, stale, true, store.Alloc{}) | 57 | hr = toHostResponse(h, stale, true, store.Alloc{}) |
| 58 | assert.Nil(t, hr.Metrics, "stale (offline-but-known) host must not serve live metrics") | 58 | assert.Nil(t, hr.Metrics, "stale (offline-but-known) host must not serve live metrics") |
internal/server/boot/boot.go
| Old | New | ||
|---|---|---|---|
| @@ -255,9 +255,10 @@ func run(cfgPath string) error { | |||
| 255 | 255 | ||
| 256 | go func() { | 256 | go func() { |
| 257 | slog.Info("quic listening", "addr", cfg.QUICListen) | 257 | slog.Info("quic listening", "addr", cfg.QUICListen) |
| 258 | if err := svc.Serve(context.Background(), lis); err != nil { | 258 | // Serve loops until the listener fails, so it never returns nil: the |
| 259 | fatal <- fmt.Errorf("quic serve: %w", err) | 259 | // only way out of the accept loop is the error being reported here. |
| 260 | } | 260 | err := svc.Serve(context.Background(), lis) |
| 261 | fatal <- fmt.Errorf("quic serve: %w", err) | ||
| 261 | }() | 262 | }() |
| 262 | 263 | ||
| 263 | // Background: finalize drained decommissioning hosts. | 264 | // Background: finalize drained decommissioning hosts. |
internal/server/boot/sshgate.go
| Old | New | ||
|---|---|---|---|
| @@ -198,9 +198,9 @@ func (g *sshGateSetup) startListener(st *store.Store, svc *syncsvc.Service, fata | |||
| 198 | } | 198 | } |
| 199 | go func() { | 199 | go func() { |
| 200 | slog.Info("ssh jump gate listening", "addr", g.listen) | 200 | slog.Info("ssh jump gate listening", "addr", g.listen) |
| 201 | if err := gate.Serve(ln); err != nil { | 201 | // Serve returns only when the listener fails, never nil. |
| 202 | fatal <- fmt.Errorf("ssh gate serve: %w", err) | 202 | err := gate.Serve(ln) |
| 203 | } | 203 | fatal <- fmt.Errorf("ssh gate serve: %w", err) |
| 204 | }() | 204 | }() |
| 205 | return nil | 205 | return nil |
| 206 | } | 206 | } |
scripts/coverage.sh
| Old | New | ||
|---|---|---|---|
| @@ -19,70 +19,79 @@ cd "$(dirname "$0")/.." | |||
| 19 | # Each floor sits just under the package's measured coverage — the largest | 19 | # Each floor sits just under the package's measured coverage — the largest |
| 20 | # integer strictly below it (a 100% package floors at 100). Raise a floor | 20 | # integer strictly below it (a 100% package floors at 100). Raise a floor |
| 21 | # whenever you raise the coverage; never lower one to make a drop pass. | 21 | # whenever you raise the coverage; never lower one to make a drop pass. |
| 22 | # | ||
| 23 | # The whole table was re-baselined for Go 1.27, which counts statements more | ||
| 24 | # finely than 1.26 did (a closure body is now its own block rather than being | ||
| 25 | # folded into the statement that declares it). No test changed: 25 floors rose | ||
| 26 | # because the finer blocks credit covered code the old accounting merged away. | ||
| 27 | # Two packages fell — internal/server/boot 33→22 and internal/gateclient 60→56 | ||
| 28 | # — because they are mostly wiring closures that nothing exercises, and 1.27 | ||
| 29 | # stopped hiding that in their denominators. Both cover MORE statements than | ||
| 30 | # they did under 1.26; only the honest denominator grew. | ||
| 22 | declare -A FLOOR=( | 31 | declare -A FLOOR=( |
| 23 | [internal/agent/reconcile]=90 | 32 | [internal/agent/reconcile]=91 |
| 24 | [internal/agent/state]=67 | 33 | [internal/agent/state]=67 |
| 25 | [internal/agent/statelock]=77 | 34 | [internal/agent/statelock]=77 |
| 26 | [internal/agent/seed]=87 | 35 | [internal/agent/seed]=90 |
| 27 | [internal/agent/ipalloc]=90 | 36 | [internal/agent/ipalloc]=90 |
| 28 | [internal/agent/hostinfo]=99 | 37 | [internal/agent/hostinfo]=99 |
| 29 | [internal/agent/imagecache]=80 | 38 | [internal/agent/imagecache]=83 |
| 30 | [internal/agent/netenv]=88 | 39 | [internal/agent/netenv]=89 |
| 31 | # netsnoop's parser, its packet filter and the direction check that decides | 40 | # netsnoop's parser, its packet filter and the direction check that decides |
| 32 | # whether a lease is real are all covered here; what is left is opening and | 41 | # whether a lease is real are all covered here; what is left is opening and |
| 33 | # binding the AF_PACKET socket, which needs CAP_NET_RAW and a live tap and is | 42 | # binding the AF_PACKET socket, which needs CAP_NET_RAW and a live tap and is |
| 34 | # proven on real hardware, so the package number stays in the 60s. | 43 | # proven on real hardware, so the package number stays in the 60s. |
| 35 | [internal/agent/netsnoop]=63 | 44 | [internal/agent/netsnoop]=63 |
| 36 | [internal/agent/cloudhv]=86 | 45 | [internal/agent/cloudhv]=89 |
| 37 | [internal/agent/hyperlog]=92 | 46 | [internal/agent/hyperlog]=93 |
| 38 | [internal/agent/pidfile]=100 | 47 | [internal/agent/pidfile]=100 |
| 39 | [internal/agent/vfkit]=88 | 48 | [internal/agent/vfkit]=90 |
| 40 | # syncclient has a load-sensitive timing test: 82.3% measured standalone, but | 49 | # syncclient has a load-sensitive timing test: 84.9% measured standalone and |
| 41 | # 81.8–83.1% under the all-package parallel run, where a loaded machine can | 50 | # in every all-package run sampled so far, but a loaded machine can cost it a |
| 42 | # cost it a branch. The floor sits under the bottom of that range rather than | 51 | # branch. The floor keeps a point of margin under that number rather than |
| 43 | # under the standalone number — a floor that only holds on an idle machine | 52 | # sitting just below it — a floor that only holds on an idle machine fails CI |
| 44 | # fails CI at random (collab issue 4cb268b3). | 53 | # at random (collab issue 4cb268b3). |
| 45 | [internal/agent/syncclient]=81 | 54 | [internal/agent/syncclient]=83 |
| 46 | # exposeproxy runs real proxy goroutines; its coverage wobbles run to run | 55 | # exposeproxy runs real proxy goroutines; its coverage wobbles run to run |
| 47 | # (measured 92.5–93.8%), so this floor carries a margin the others don't need. | 56 | # (measured 91.8–93.0%), so this floor carries a margin the others don't need. |
| 48 | [internal/agent/exposeproxy]=91 | 57 | [internal/agent/exposeproxy]=90 |
| 49 | [internal/agent/serialpump]=85 | 58 | [internal/agent/serialpump]=86 |
| 50 | [internal/agent/enrollclient]=81 | 59 | [internal/agent/enrollclient]=83 |
| 51 | [internal/agent/dhcp]=64 | 60 | [internal/agent/dhcp]=68 |
| 52 | [internal/agent/permanent]=100 | 61 | [internal/agent/permanent]=100 |
| 53 | [internal/server/api]=83 | 62 | [internal/server/api]=84 |
| 54 | [internal/server/delegation]=91 | 63 | [internal/server/delegation]=91 |
| 55 | [internal/server/seal]=90 | 64 | [internal/server/seal]=90 |
| 56 | [internal/server/sshca]=72 | 65 | [internal/server/sshca]=72 |
| 57 | [internal/server/sshgate]=87 | 66 | [internal/server/sshgate]=89 |
| 58 | [internal/server/mcphttp]=93 | 67 | [internal/server/mcphttp]=94 |
| 59 | [internal/server/vmssh]=100 | 68 | [internal/server/vmssh]=100 |
| 60 | [internal/server/boot]=33 | 69 | [internal/server/boot]=22 |
| 61 | [internal/server/health]=100 | 70 | [internal/server/health]=100 |
| 62 | [internal/server/api/client]=93 | 71 | [internal/server/api/client]=93 |
| 63 | [internal/server/api/spec]=93 | 72 | [internal/server/api/spec]=93 |
| 64 | [internal/server/store]=80 | 73 | [internal/server/store]=81 |
| 65 | [internal/server/registry]=100 | 74 | [internal/server/registry]=100 |
| 66 | [internal/server/release]=93 | 75 | [internal/server/release]=93 |
| 67 | [internal/agent/selfupdate]=70 | 76 | [internal/agent/selfupdate]=71 |
| 68 | [internal/agent/bootstrap]=73 | 77 | [internal/agent/bootstrap]=75 |
| 69 | [internal/agent/run]=48 | 78 | [internal/agent/run]=48 |
| 70 | [internal/server/hosttoken]=100 | 79 | [internal/server/hosttoken]=100 |
| 71 | [internal/server/hub]=94 | 80 | [internal/server/hub]=94 |
| 72 | [internal/server/syncsvc]=85 | 81 | [internal/server/syncsvc]=87 |
| 73 | [internal/server/web]=95 | 82 | [internal/server/web]=95 |
| 74 | [internal/transport]=82 | 83 | [internal/transport]=83 |
| 75 | [internal/shape]=91 | 84 | [internal/shape]=92 |
| 76 | [internal/site]=84 | 85 | [internal/site]=87 |
| 77 | [internal/smoke]=56 | 86 | [internal/smoke]=57 |
| 78 | [internal/cli]=71 | 87 | [internal/cli]=72 |
| 79 | [internal/mcpserver]=76 | 88 | [internal/mcpserver]=77 |
| 80 | [internal/oidcprovider]=79 | 89 | [internal/oidcprovider]=81 |
| 81 | [internal/server/config]=100 | 90 | [internal/server/config]=100 |
| 82 | [internal/cloudinit]=77 | 91 | [internal/cloudinit]=77 |
| 83 | [internal/covsnap]=77 | 92 | [internal/covsnap]=77 |
| 84 | [internal/joinblob]=96 | 93 | [internal/joinblob]=96 |
| 85 | [internal/gateclient]=60 | 94 | [internal/gateclient]=56 |
| 86 | [internal/names]=74 | 95 | [internal/names]=74 |
| 87 | # internal/random has no tests of its own: it is exercised only through the | 96 | # internal/random has no tests of its own: it is exercised only through the |
| 88 | # packages that call it, so it reports 0.0% here. A real floor would be a | 97 | # packages that call it, so it reports 0.0% here. A real floor would be a |