From 18a9f6624e8f4fa38b27f408f1f989aa16552fae Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Sun, 3 May 2026 15:00:24 +0100 Subject: [PATCH] ci: migrate .golangci.yml to v2 schema + only-new-issues gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bump from golangci-lint-action@v6 → v7 (which downloads the v2.x binary) was blocking CI lint with 'unsupported version of the configuration: ""' because .golangci.yml was still in the v1 schema. Migrate the config to v2: * version: "2" prelude * disable-all → default: none * linters-settings → linters.settings * gofumpt + goimports move into formatters.enable + formatters.settings * exclude-rules move into linters.exclusions.rules * gosimple drops (folded into staticcheck in v2) Fix the four lint hits in the new P2R-02 code: * host_bandwidth.go: convert hostBandwidthRequest directly to hostBandwidthView via type conversion (S1016) * ui_repo.go: drop unparam savedSection + status arguments from renderRepoPage (always "" / always 422 — split GET render from validation-fail render) * ui_schedules.go: gofumpt formatting on the scheduleEditPage struct Add only-new-issues: true to the lint job. The repo carries ~90 pre-existing findings (gofumpt drift × 31, misspell × 25, missing godoc × 10, bodyclose × 6, errcheck × 12, …) accumulated before lint was actually wired into CI. Without this gate, every PR would fail on baseline noise instead of its own changes. Track the cleanup as X-06 in tasks.md so the gate is temporary. --- .gitea/workflows/ci.yml | 7 +++++ .golangci.yml | 42 +++++++++++++++----------- internal/server/http/host_bandwidth.go | 5 +-- internal/server/http/ui_repo.go | 34 ++++++++++----------- internal/server/http/ui_schedules.go | 14 ++++----- tasks.md | 1 + 6 files changed, 56 insertions(+), 47 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 091b753..a3c90fd 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -41,6 +41,13 @@ jobs: # Bumping to a v2.x release built against current Go. version: v2.1.6 args: --timeout=5m + # Only flag issues introduced by the PR. The repo carries + # ~90 pre-existing findings (mostly missing godoc comments + # + gofumpt drift + misspell) accumulated before lint was + # actually wired into CI; cleaning them up is its own piece + # of work tracked separately. Without this, every PR fails + # on baseline noise instead of its own changes. + only-new-issues: true build: name: Build (${{ matrix.goos }}/${{ matrix.goarch }}) diff --git a/.golangci.yml b/.golangci.yml index 45a99f6..787123f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,18 +1,17 @@ +version: "2" + run: timeout: 5m tests: true linters: - disable-all: true + default: none enable: - errcheck - - gosimple - govet - ineffassign - staticcheck - unused - - gofumpt - - goimports - misspell - revive - bodyclose @@ -21,22 +20,29 @@ linters: - prealloc - unconvert - unparam - -linters-settings: - goimports: - local-prefixes: gitea.dcglab.co.uk/steve/restic-manager - revive: + settings: + revive: + rules: + - name: exported + arguments: ["disableStutteringCheck"] + misspell: + locale: US + exclusions: rules: - - name: exported - arguments: ["disableStutteringCheck"] - misspell: - locale: US + - path: _test\.go + linters: + - errcheck + - unparam + +formatters: + enable: + - gofumpt + - goimports + settings: + goimports: + local-prefixes: + - gitea.dcglab.co.uk/steve/restic-manager issues: - exclude-rules: - - path: _test\.go - linters: - - errcheck - - unparam max-issues-per-linter: 0 max-same-issues: 0 diff --git a/internal/server/http/host_bandwidth.go b/internal/server/http/host_bandwidth.go index 6b3172c..e42996b 100644 --- a/internal/server/http/host_bandwidth.go +++ b/internal/server/http/host_bandwidth.go @@ -58,8 +58,5 @@ func (s *Server) handleUpdateHostBandwidth(w stdhttp.ResponseWriter, r *stdhttp. writeJSONError(w, stdhttp.StatusInternalServerError, "internal", err.Error()) return } - writeJSON(w, stdhttp.StatusOK, hostBandwidthView{ - BandwidthUpKBps: req.BandwidthUpKBps, - BandwidthDownKBps: req.BandwidthDownKBps, - }) + writeJSON(w, stdhttp.StatusOK, hostBandwidthView(req)) } diff --git a/internal/server/http/ui_repo.go b/internal/server/http/ui_repo.go index b9e334e..e0d54d6 100644 --- a/internal/server/http/ui_repo.go +++ b/internal/server/http/ui_repo.go @@ -153,25 +153,23 @@ func (s *Server) handleUIHostRepo(w stdhttp.ResponseWriter, r *stdhttp.Request) } // renderRepoFormError loads the page state, overlays the section's -// error / saved marker, and renders. Returns an HTTP status (422 for -// validation, 200 for success). -func (s *Server) renderRepoPage(w stdhttp.ResponseWriter, r *stdhttp.Request, u *ui.User, host *store.Host, savedSection, credErr, bwErr, mntErr string, status int) { +// error banner, and renders with a 422. Save-success goes through a +// 303 redirect with `?saved=
` instead, so this path is for +// validation failures only. +func (s *Server) renderRepoPage(w stdhttp.ResponseWriter, r *stdhttp.Request, u *ui.User, host *store.Host, credErr, bwErr, mntErr string) { page, err := s.loadHostRepoPage(r, *host) if err != nil { slog.Error("ui repo: reload after save", "host_id", host.ID, "err", err) stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError) return } - page.SavedSection = savedSection page.CredentialsError = credErr page.BandwidthError = bwErr page.MaintenanceError = mntErr view := s.baseView(u, "dashboard") view.Title = host.Name + " repo · restic-manager" view.Page = *page - if status != stdhttp.StatusOK { - w.WriteHeader(status) - } + w.WriteHeader(stdhttp.StatusUnprocessableEntity) if err := s.deps.UI.Render(w, "host_repo", view); err != nil { slog.Error("ui: render host_repo", "err", err) } @@ -200,7 +198,7 @@ func (s *Server) handleUIRepoCredentialsSave(w stdhttp.ResponseWriter, r *stdhtt repoPass := r.PostForm.Get("repo_password") // do NOT trim — operators may use trailing space deliberately if repoURL == "" { - s.renderRepoPage(w, r, u, host, "", "Repo URL is required.", "", "", stdhttp.StatusUnprocessableEntity) + s.renderRepoPage(w, r, u, host, "Repo URL is required.", "", "") return } @@ -217,9 +215,9 @@ func (s *Server) handleUIRepoCredentialsSave(w stdhttp.ResponseWriter, r *stdhtt existing.RepoPassword = repoPass } if existing.RepoPassword == "" { - s.renderRepoPage(w, r, u, host, "", + s.renderRepoPage(w, r, u, host, "No password on file yet — set one before saving the URL/username.", - "", "", stdhttp.StatusUnprocessableEntity) + "", "") return } @@ -258,9 +256,9 @@ func (s *Server) handleUIRepoBandwidthSave(w stdhttp.ResponseWriter, r *stdhttp. up, upErr := parseOptionalNonNegInt(r.PostForm.Get("bandwidth_up")) down, downErr := parseOptionalNonNegInt(r.PostForm.Get("bandwidth_down")) if upErr != nil || downErr != nil { - s.renderRepoPage(w, r, u, host, "", "", + s.renderRepoPage(w, r, u, host, "", "Bandwidth caps must be non-negative whole numbers (or blank for no cap).", - "", stdhttp.StatusUnprocessableEntity) + "") return } if err := s.deps.Store.SetHostBandwidth(r.Context(), host.ID, up, down); err != nil { @@ -296,20 +294,20 @@ func (s *Server) handleUIRepoMaintenanceSave(w stdhttp.ResponseWriter, r *stdhtt "forget": forgetCron, "prune": pruneCron, "check": checkCron, } { if expr == "" { - s.renderRepoPage(w, r, u, host, "", "", "", - label+" cadence is required.", stdhttp.StatusUnprocessableEntity) + s.renderRepoPage(w, r, u, host, "", "", + label+" cadence is required.") return } if _, err := cronParser.Parse(expr); err != nil { - s.renderRepoPage(w, r, u, host, "", "", "", - label+" cadence didn't parse: "+err.Error(), stdhttp.StatusUnprocessableEntity) + s.renderRepoPage(w, r, u, host, "", "", + label+" cadence didn't parse: "+err.Error()) return } } subset, err := strconv.Atoi(subsetStr) if err != nil || subset < 0 || subset > 100 { - s.renderRepoPage(w, r, u, host, "", "", "", - "check subset % must be between 0 and 100.", stdhttp.StatusUnprocessableEntity) + s.renderRepoPage(w, r, u, host, "", "", + "check subset % must be between 0 and 100.") return } diff --git a/internal/server/http/ui_schedules.go b/internal/server/http/ui_schedules.go index 61f9e64..d17ff0d 100644 --- a/internal/server/http/ui_schedules.go +++ b/internal/server/http/ui_schedules.go @@ -39,13 +39,13 @@ type scheduleFormData struct { // scheduleEditPage backs both the new and edit form views. type scheduleEditPage struct { hostChromeData - IsNew bool - ScheduleID string // empty when IsNew - Form scheduleFormData - AvailableGroups []store.SourceGroup - SelectedGroupIDs map[string]bool // gid → checked - SaveAction string - Error string + IsNew bool + ScheduleID string // empty when IsNew + Form scheduleFormData + AvailableGroups []store.SourceGroup + SelectedGroupIDs map[string]bool // gid → checked + SaveAction string + Error string } func (s *Server) handleUISchedulesList(w stdhttp.ResponseWriter, r *stdhttp.Request) { diff --git a/tasks.md b/tasks.md index df03958..632454d 100644 --- a/tasks.md +++ b/tasks.md @@ -273,3 +273,4 @@ Sizes: **S** = under a day, **M** = 1–3 days, **L** = 3–7 days. - [ ] **X-03** Periodic dependency updates (`dependabot` or `renovate`) - [ ] **X-04** Threat-model review at end of each phase - [ ] **X-05** Proper first-run onboarding UI: admin shouldn't need to `curl` `/api/bootstrap` by hand. Render the bootstrap form on the same login page (extra "setup token" field shown only while no admin user exists, hidden after); on submit POST to `/api/bootstrap`, then drop straight into a session. Surface the one-time token from the server log somewhere copy-able (or print a clickable URL with the token in the query string at first-run). Also: relax the 12-char password floor for the first-run path or document it in the form so `admin` doesn't silently fail validation. +- [ ] **X-06** Lint-baseline cleanup pass. `.golangci.yml` is now on the v2 schema; CI is gated with `only-new-issues: true` because the repo carries ~90 pre-existing findings (gofumpt drift × 31, misspell × 25, missing godoc on exported consts × 10, bodyclose × 6, errcheck × 12, errorlint/nilerr/unused × handful) accumulated before lint was actually wired into CI. Drive the count to zero in a dedicated PR (mostly mechanical: `gofumpt -w .`, fix typos, add comments, audit nilerr cases since those *might* be real bugs), then drop `only-new-issues: true` so future regressions are caught at the source.