lint: drive baseline to zero, drop only-new-issues gate

Cleanup pass over the repo so CI can enforce lint going forward
without the only-new-issues escape hatch:

* gofumpt -w across the tree (31 hits, all formatting)
* misspell --fix (25 hits, US-locale spelling) — but reverted on
  api.JobCancelled = "cancelled" since that literal is the wire +
  DB CHECK constraint value, plus matched the case in store/fleet.go
  back to "cancelled" and added //nolint:misspell on both for the
  next time someone reaches for the auto-fix
* Wrap every `defer rows.Close()` / `defer stmt.Close()` /
  `defer res.Body.Close()` in `defer func() { _ = .Close() }()`
  to satisfy errcheck without losing the close itself
* websocket.Dial callers (1 prod, 4 tests) now capture + close the
  upgrade response Body — coder/websocket can return res with a nil
  Body on success, so the test deferred-closes guard against that
* Annotate the two genuine-by-design nilerr cases with //nolint
  comments explaining why nil-on-error is the contract (cookie
  missing = no session; ctx cancelled mid-backoff = clean shutdown)
* Add brief godoc on the 10 exported const groups + types that
  revive flagged (api.HostOS/HostArch/JobKind/JobStatus/LogStream/
  ErrorCode, restic.EventKind, store.Role, web.FS)
* Drop the unused (*Server).userByID method
* Inline the unparam baseView(active) — every UI page is under
  the dashboard primary nav today

Result: `golangci-lint run ./...` reports 0 issues. CI lint job
no longer needs only-new-issues: true; X-06 follow-up entry in
tasks.md removed.
This commit is contained in:
2026-05-03 16:15:17 +01:00
parent 41c3ec7c6f
commit b6f8de1dcc
54 changed files with 317 additions and 260 deletions
+7 -7
View File
@@ -99,13 +99,13 @@ func (r *Runner) RunBackup(ctx context.Context, jobID string, paths, excludes, t
}
lastProgress = time.Now()
progEnv, _ := api.Marshal(api.MsgJobProgress, jobID, api.JobProgressPayload{
JobID: jobID,
PercentDone: status.PercentDone,
FilesDone: status.FilesDone,
TotalFiles: status.TotalFiles,
BytesDone: status.BytesDone,
TotalBytes: status.TotalBytes,
ETASeconds: status.SecondsRem,
JobID: jobID,
PercentDone: status.PercentDone,
FilesDone: status.FilesDone,
TotalFiles: status.TotalFiles,
BytesDone: status.BytesDone,
TotalBytes: status.TotalBytes,
ETASeconds: status.SecondsRem,
ThroughputBps: throughput(status.BytesDone, status.SecondsElapsed),
})
_ = r.tx.Send(progEnv)
+1 -2
View File
@@ -110,7 +110,7 @@ func (s *Scheduler) Apply(payload api.ScheduleSetPayload, tx Sender) {
"received", len(payload.Schedules), "active", added)
// Ack outside the lock — Send() shouldn't take long, but holding
// s.mu across an external call would needlessly serialise other
// s.mu across an external call would needlessly serialize other
// callers (e.g. a future Status() inspection from the UI).
ackEnv, err := api.Marshal(api.MsgScheduleAck, "", api.ScheduleAckPayload{
Version: payload.Version,
@@ -167,4 +167,3 @@ func (s *Scheduler) fire(entry api.Schedule) {
"schedule_id", entry.ID, "err", err)
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ import (
// additionalData binds ciphertexts to the agent-secrets context, so a
// blob lifted from one role's file can't be replayed into another's
// row in some unrelated table that uses the same key. (Defence in
// row in some unrelated table that uses the same key. (Defense in
// depth — the key is per-host today, but cheap to be careful.)
const additionalData = "rm-agent-repo-creds-v1"
+4 -2
View File
@@ -48,7 +48,9 @@ func Collect(ctx context.Context, resticPath string) (Snapshot, error) {
// detectResticVersion runs `restic version` and parses the first line.
// Output looks like:
// restic 0.17.1 compiled with go1.22.5 on linux/amd64
//
// restic 0.17.1 compiled with go1.22.5 on linux/amd64
//
// Returns the version token (e.g. "0.17.1") or "" if restic isn't
// found. We never block startup on a missing restic — the operator
// might not have installed it yet, and the agent should still be
@@ -74,5 +76,5 @@ func detectResticVersion(ctx context.Context, override string) (string, error) {
if len(parts) >= 2 && parts[0] == "restic" {
return parts[1], nil
}
return "", fmt.Errorf("sysinfo: unrecognised restic version output: %q", first)
return "", fmt.Errorf("sysinfo: unrecognized restic version output: %q", first)
}
+11 -4
View File
@@ -40,7 +40,7 @@ type Config struct {
// Sender is what handlers use to push agent → server messages
// (job.progress, job.finished, log.stream, command.result, …).
// Returned by the WS client to the dispatch handler. Write operations
// serialise behind a single mutex on the conn; concurrent calls are
// serialize behind a single mutex on the conn; concurrent calls are
// safe.
type Sender interface {
Send(env api.Envelope) error
@@ -52,7 +52,7 @@ type Sender interface {
type Handler func(ctx context.Context, env api.Envelope, tx Sender) error
// Run keeps the agent connected indefinitely. Returns when ctx is
// cancelled. Errors during a single connection attempt are logged and
// canceled. Errors during a single connection attempt are logged and
// trigger reconnect-with-backoff; only ctx.Done() ends the loop.
func Run(ctx context.Context, cfg Config, handle Handler) error {
if cfg.HeartbeatPeriod <= 0 {
@@ -69,7 +69,10 @@ func Run(ctx context.Context, cfg Config, handle Handler) error {
slog.Warn("ws agent disconnect", "err", err)
}
if err := sleepCtx(ctx, backoff.next()); err != nil {
return nil
// ctx cancellation mid-backoff means the parent shut us down —
// exit the reconnect loop quietly rather than propagating
// a context error up to a caller that will discard it.
return nil //nolint:nilerr
}
}
}
@@ -100,11 +103,15 @@ func connectOnce(ctx context.Context, cfg Config, handle Handler) error {
}
dialCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
conn, _, err := websocket.Dial(dialCtx, wsURL, dialOpts)
conn, res, err := websocket.Dial(dialCtx, wsURL, dialOpts)
cancel()
if err != nil {
return fmt.Errorf("dial: %w", err)
}
// websocket.Dial returns the upgrade response separately from the
// conn. Body is empty on a successful upgrade but Go's net/http
// still expects it closed to release the connection.
defer func() { _ = res.Body.Close() }()
defer conn.CloseNow() //nolint:errcheck
// Send hello.
+1 -1
View File
@@ -50,7 +50,7 @@ func Enroll(ctx context.Context, serverURL string, req EnrollRequest) (*EnrollRe
if err != nil {
return nil, fmt.Errorf("agent enroll: post: %w", err)
}
defer res.Body.Close()
defer func() { _ = res.Body.Close() }()
rawRes, _ := io.ReadAll(res.Body)
if res.StatusCode != stdhttp.StatusCreated {
return nil, fmt.Errorf("agent enroll: server returned %d: %s",