Pin golangci-lint to commit c0d3ddc9cf3faa61a4e378e879ece580256d76e5
(v2.12.2) in Dockerfile and script/bootstrap.
Set .golangci.yml to the org-standard v2-schema config already used
across the org's repos (owner-authorized; the same file is being
landed as canonical via a prompts-repo PR). Lint settings live under
linters.settings, so the lll, funlen, cyclop, and dupl thresholds are
actually applied. The informational gomodguard deprecation warning
this config can emit under v2.12 is accepted.
Fix all findings surfaced by the now-active thresholds:
- goconst: shared constants for repeated status, priority, and DNS
fixture strings in watcher.go and the notify, state, and watcher
tests
- dupl: consolidate duplicated ntfy/slack HTTP-error tests and
SendNotification endpoint-error tests behind shared helpers
- lll: wrap long test table entries and comments; shorten one inline
nolint justification
## Summary
Port state keys are `ip:port` with a single `hostname` field. When multiple hostnames resolve to the same IP (shared hosting, CDN), only one hostname was associated. This caused orphaned port state when that hostname removed the IP from DNS while the IP remained valid for other hostnames.
## Changes
### State (`internal/state/state.go`)
- `PortState.Hostname` (string) → `PortState.Hostnames` ([]string)
- Custom `UnmarshalJSON` for backward compatibility: reads old single `hostname` field and migrates to a single-element `hostnames` slice
- Added `DeletePortState` and `GetAllPortKeys` methods for cleanup
### Watcher (`internal/watcher/watcher.go`)
- Refactored `checkAllPorts` into three phases:
1. Build IP:port → hostname associations from current DNS data
2. Check each unique IP:port once with all associated hostnames
3. Clean up stale port state entries with no hostname references
- Port change notifications now list all associated hostnames (`Hosts:` instead of `Host:`)
- Added `buildPortAssociations`, `parsePortKey`, and `cleanupStalePorts` helper functions
### README
- Updated state file format example: `hostname` → `hostnames` (array)
- Updated notification description to reflect multiple hostnames
## Backward Compatibility
Existing state files with the old single `hostname` string are handled gracefully via custom JSON unmarshaling — they are read as single-element `hostnames` slices.
Closes #55
Co-authored-by: clawbot <clawbot@noreply.eeqj.de>
Reviewed-on: #65
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
State.Save() was using RLock but mutating s.snapshot.LastUpdated,
which is a write operation. This created a data race since other
goroutines could also hold a read lock and observe a partially
written timestamp. Changed to full Lock to ensure exclusive access
during the mutation.
Full project structure following upaas conventions: uber/fx DI, go-chi
routing, slog logging, Viper config. State persisted as JSON file with
per-nameserver record tracking for inconsistency detection. Stub
implementations for resolver, portcheck, tlscheck, and watcher.