Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ on:
permissions:
contents: read

# Pre-empt the September 2026 Node.js 20 → 24 transition by opting in now.
# Without this, every action run in this workflow logs a deprecation banner
# and will hard-fail once Node 20 is removed from the runner image. See:
# https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'

jobs:
build:
runs-on: ubuntu-latest
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ permissions:
id-token: write # for cosign keyless OIDC signing
packages: write

# Pre-empt the September 2026 Node.js 20 → 24 transition. See
# .github/workflows/ci.yml for the rationale.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'

jobs:
release:
runs-on: ubuntu-latest
Expand Down
58 changes: 58 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,64 @@ _No unreleased changes._

---

## 26.09 — 2026-05-27

Post-Planning.md tightening pass — clears the lint debt that 26.06's
`golangci-lint` integration (item #40) catches but never fixed, and
pre-empts the September 2026 Node.js 20 → 24 transition on GitHub
Actions before it bites.

### Tooling

- **`golangci-lint run ./...` now passes clean.** The 21 baseline
findings (15× errcheck on `Close`/`Body.Close`, 4× staticcheck
QF1008/QF1012, 2× gocritic ifElseChain/exitAfterDefer) are all
resolved — either by explicit `_ =` discards on Close calls in defer
position (the Go idiom for "we don't care about this error") or by
rewriting the offending pattern. `make lint` is now meaningfully
enforceable.
- **`cmd/console` `os.Exit` no longer skips deferred cleanup**
(gocritic exitAfterDefer). The real entry point moved into a `run()
int` helper and `main` calls `os.Exit(run())`, matching the
established Go idiom.
- **`config.go` `MarshalJSON` drops the redundant `.Duration` selector**
(staticcheck QF1008).
- **`cmd/console/tui` `WriteString(fmt.Sprintf(...))` rewritten to
`fmt.Fprintf(&b, ...)`** (staticcheck QF1012) at three sites.
- **`render()` `loading`/`err`/default chain rewritten as `switch`**
(gocritic ifElseChain).

### Added

- **`renovate.json`** — Renovate Bot configuration for ongoing
supply-chain updates. Bundles `gomod` minor/patch into one weekly PR
(`go-deps`), auto-merges GitHub Actions and Docker base-image digest
bumps, schedules `lockFileMaintenance` monthly, and labels
vulnerability alerts immediately. No automerges on `gomod` majors —
those land as manually-reviewed PRs.
- **`internal/tracing/tracing_test.go`** — covers `Setup` with an
empty endpoint, confirms `HTTPMiddleware` produces a valid span
inside the handler context, and verifies `HTTPClient` wraps an
injected base RoundTripper instead of replacing it.

### Changed

- **GitHub Actions opt in to Node.js 24 now.** Both `ci.yml` and
`release.yml` set `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true`,
pre-empting the September 2026 Node.js 20 removal that would
otherwise hard-fail CI without warning.
- **`internal/tracing` semconv bumped from v1.26.0 → v1.40.0** to
match the otel SDK's `resource.Default()` schema URL — the older
version produced "conflicting Schema URL" errors at `Setup` time.

### Notes

- No new Planning.md items remain. This sprint addresses real lint and
CI-deprecation debt rather than feature work; future cleanup
follow-ups should be tracked via issues or a new Planning.md entry.

---

## 26.08 — 2026-05-27

Final Planning.md sprint. Items #13 (peer TLS), #20 (OpenTelemetry tracing),
Expand Down
18 changes: 13 additions & 5 deletions cmd/console/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@ import (
)

func main() {
os.Exit(run())
}

// run holds the real entry-point logic so the os.Exit lives in main —
// otherwise deferred cleanup (signal stop, db.Close) would be skipped on
// the error paths (gocritic exitAfterDefer).
func run() int {
dbPath := flag.String("db", "inventory.db", "path to SQLite database file")
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()

if *showVersion {
fmt.Fprintf(os.Stdout, "console %s\n", runtime.VersionString())
return
fmt.Printf("console %s\n", runtime.VersionString())
return 0
}

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
Expand All @@ -38,14 +45,15 @@ func main() {
db, err := sqlite.Open(ctx, *dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "console: open database %q: %v\n", *dbPath, err)
os.Exit(1)
return 1
}
defer db.Close()
defer func() { _ = db.Close() }()

m := tui.New(ctx, db.Hosts(), db.Ports(), db.Scans())
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithContext(ctx))
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "console: %v\n", err)
os.Exit(1)
return 1
}
return 0
}
19 changes: 10 additions & 9 deletions cmd/console/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,11 +498,12 @@ func (m Model) View() string {
b.WriteString(m.renderTabs())
b.WriteString("\n")

if m.loading {
switch {
case m.loading:
b.WriteString("\n " + m.spin.View() + " Loading…\n")
} else if m.err != nil {
case m.err != nil:
b.WriteString(lipgloss.NewStyle().Foreground(colRed).Render(" Error: "+m.err.Error()) + "\n")
} else {
default:
switch m.current {
case viewDashboard:
b.WriteString(m.renderDashboard())
Expand Down Expand Up @@ -607,24 +608,24 @@ func renderScanRows(scans []*models.Scan) string {
if s.FinishedAt != nil {
status = styleStatusDone.Render("done")
}
b.WriteString(fmt.Sprintf(" %-20s hosts:%-4d started:%-22s %s\n",
fmt.Fprintf(&b, " %-20s hosts:%-4d started:%-22s %s\n",
s.Subnet,
s.HostsFound,
fmtTime(s.StartedAt),
status,
))
)
}
return b.String()
}

func renderHostRows(hosts []*models.Host) string {
var b strings.Builder
for _, h := range hosts {
b.WriteString(fmt.Sprintf(" %-16s %-22s %s\n",
fmt.Fprintf(&b, " %-16s %-22s %s\n",
h.IPAddress,
orDash(h.Hostname),
fmtTime(h.LastSeen),
))
)
}
return b.String()
}
Expand Down Expand Up @@ -655,10 +656,10 @@ func (m Model) renderHostDetail() string {
{"Last Seen", fmtTime(h.LastSeen)},
}
for _, kv := range meta {
b.WriteString(fmt.Sprintf(" %-18s %s\n",
fmt.Fprintf(&b, " %-18s %s\n",
styleCardLabel.Render(kv.k+":"),
kv.v,
))
)
}
b.WriteString("\n")
b.WriteString(styleSectionHeader.Render(fmt.Sprintf("Open Ports — %d found", len(m.portList))) + "\n")
Expand Down
2 changes: 1 addition & 1 deletion cmd/internal/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func Run(opts Options) int {
flag.Parse()

if *showVersion {
fmt.Fprintf(os.Stdout, "%s %s\n", opts.Name, VersionString())
_, _ = fmt.Fprintf(os.Stdout, "%s %s\n", opts.Name, VersionString())
return 0
}

Expand Down
4 changes: 2 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
}

func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Duration.String())
return json.Marshal(d.String())
}

// Config is the top-level configuration object.
Expand Down Expand Up @@ -236,7 +236,7 @@ func Load(path string) (*Config, error) {
return nil, fmt.Errorf("open config %q: %w", path, err)
}
if err == nil {
defer f.Close()
defer func() { _ = f.Close() }()
if info, statErr := f.Stat(); statErr == nil {
fileMode = info.Mode().Perm()
loaded = true
Expand Down
4 changes: 2 additions & 2 deletions internal/health/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (c *Client) Ping(ctx context.Context) error {
if err != nil {
return fmt.Errorf("health check failed: %w", err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
_, _ = io.Copy(io.Discard, resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("peer returned status %d", resp.StatusCode)
Expand All @@ -93,7 +93,7 @@ func (c *Client) FetchStatus(ctx context.Context) (Status, error) {
if err != nil {
return Status{}, fmt.Errorf("fetch status failed: %w", err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

var s Status
if err := json.NewDecoder(io.LimitReader(resp.Body, maxStatusBytes)).Decode(&s); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/scanner/arp.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func lookupARP(ip string) (string, string) {
if err != nil {
return "", ""
}
defer f.Close()
defer func() { _ = f.Close() }()

scanner := bufio.NewScanner(f)
scanner.Scan() // header
Expand Down
10 changes: 5 additions & 5 deletions internal/scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ func (s *Scanner) probe(ctx context.Context, ip string) (int, bool) {
results <- result{}
return
}
conn.Close()
_ = conn.Close()
results <- result{port: port, ok: true}
}(port)
}
Expand Down Expand Up @@ -294,7 +294,7 @@ func (s *Scanner) deepScan(ctx context.Context, hostID int64, ip string, knownOp
if err != nil {
return
}
conn.Close()
_ = conn.Close()
s.upsertPort(ctx, hostID, ip, port, models.TCP, models.StateOpen, ts)
}(port)
}
Expand Down Expand Up @@ -339,7 +339,7 @@ func probeUDP(ctx context.Context, ip string, port int, timeout time.Duration) (
// A connect-time error on UDP is unusual but treat it as filtered.
return "", false
}
defer conn.Close()
defer func() { _ = conn.Close() }()
// Send a single zero byte. Many services respond to anything (DNS
// returns FORMERR, NTP rejects); for the rest we still learn the
// closed-vs-filtered distinction from the read error.
Expand Down Expand Up @@ -399,7 +399,7 @@ func sshBanner(ctx context.Context, ip string, port int, timeout time.Duration)
if err != nil {
return ""
}
defer conn.Close()
defer func() { _ = conn.Close() }()
_ = conn.SetReadDeadline(time.Now().Add(timeout))
line, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
Expand All @@ -425,7 +425,7 @@ func httpServerHeader(ctx context.Context, ip string, port int, timeout time.Dur
if err != nil {
return ""
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
if s := resp.Header.Get("Server"); s != "" {
return "HTTP: " + s
}
Expand Down
6 changes: 3 additions & 3 deletions internal/sqlite/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func Open(ctx context.Context, path string) (*DB, error) {
}

if err := migrations.Run(ctx, writer); err != nil {
writer.Close()
_ = writer.Close()
return nil, fmt.Errorf("run migrations: %w", err)
}

Expand All @@ -58,7 +58,7 @@ func Open(ctx context.Context, path string) (*DB, error) {
}
reader, err := openPool(ctx, path, readers)
if err != nil {
writer.Close()
_ = writer.Close()
return nil, fmt.Errorf("open reader pool: %w", err)
}

Expand All @@ -79,7 +79,7 @@ func openPool(ctx context.Context, path string, maxOpen int) (*sql.DB, error) {
}
for _, p := range pragmas {
if _, err := conn.ExecContext(ctx, p); err != nil {
conn.Close()
_ = conn.Close()
return nil, fmt.Errorf("set %q: %w", p, err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/sqlite/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (r *HostRepo) List(ctx context.Context) ([]*models.Host, error) {
if err != nil {
return nil, fmt.Errorf("list hosts: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

var hosts []*models.Host
for rows.Next() {
Expand Down
2 changes: 1 addition & 1 deletion internal/sqlite/migrations/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func Run(ctx context.Context, db *sql.DB) error {
if err != nil {
return fmt.Errorf("query applied migrations: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

applied := make(map[string]struct{})
for rows.Next() {
Expand Down
2 changes: 1 addition & 1 deletion internal/sqlite/port.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (r *PortRepo) ListByHost(ctx context.Context, hostID int64) ([]*models.Port
if err != nil {
return nil, fmt.Errorf("list ports for host %d: %w", hostID, err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

var ports []*models.Port
for rows.Next() {
Expand Down
2 changes: 1 addition & 1 deletion internal/sqlite/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (r *ScanRepo) List(ctx context.Context) ([]*models.Scan, error) {
if err != nil {
return nil, fmt.Errorf("list scans: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

var scans []*models.Scan
for rows.Next() {
Expand Down
2 changes: 1 addition & 1 deletion internal/tracing/tracing.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
)

// Setup installs a global TracerProvider with an OTLP/HTTP exporter and the
Expand Down
Loading
Loading