ci: migrate .golangci.yml to v2 schema + only-new-issues gate
CI / Test (linux/amd64) (pull_request) Successful in 29s
CI / Lint (pull_request) Failing after 16s
CI / Build (windows/amd64) (pull_request) Successful in 20s
CI / Build (linux/amd64) (pull_request) Successful in 20s
CI / Build (linux/arm64) (pull_request) Successful in 21s

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.
This commit is contained in:
2026-05-03 15:00:24 +01:00
parent 2a8dd1eba2
commit 18a9f6624e
6 changed files with 56 additions and 47 deletions
+7
View File
@@ -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 }})
+24 -18
View File
@@ -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
+1 -4
View File
@@ -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))
}
+16 -18
View File
@@ -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=<section>` 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
}
+7 -7
View File
@@ -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) {
+1
View File
@@ -273,3 +273,4 @@ Sizes: **S** = under a day, **M** = 13 days, **L** = 37 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.