FMG-2: upgrade Go to 1.27.1, refresh dependencies and drop gotoolkit #8

Merged
fmartingr merged 4 commits from butterrobot/fmg-2-dependency-upgrade into main 2026-09-07 21:27:04 +02:00 AGit

2026-09-07

fix: close the go-smtp drain race and pin the shutdown budget by behaviour
All checks were successful
CI / goreleaser-lint (pull_request) Successful in 3s
CI / format (pull_request) Successful in 3m23s
CI / lint (pull_request) Successful in 3m42s
CI / test (pull_request) Successful in 2m28s
CI / build (pull_request) Successful in 4m11s
go-smtp registers each accepted session on a WaitGroup that Shutdown
waits on from another goroutine, so an Accept still in flight when the
drain begins is an Add concurrent with that Wait. Under -race, which is
how make test runs, the suite failed 3/20 locally. Stop accepting and
let Serve return before draining, which removes the overlap entirely:
0/25 with no data race reported. It also means a session accepted in
that instant is now actually waited for.

Closing the listener first makes Serve return net.ErrClosed and makes
Shutdown's own close fail the same way, so both are filtered — without
that, every clean shutdown logged a spurious "still in flight" warning.

The timeout half of the earlier fix was pinned only by a constant
assertion: a budget one millisecond over readTimeout, or reverting the
warn back to a returned error, both left the whole suite green. The
budget is now a Server field so a test can drive a real overrun, and
TestServerReportsNoErrorWhenDrainOverrunsBudget asserts Start returns
nil on it; the constant assertion now requires a 10s margin rather than
a strict inequality. Both reverts fail these tests.

Also:
- Warn about config keys the decode ignored, instead of dropping them
  silently. A mistyped [[Recipient]] alongside a valid [CatchAll] used
  to start with recipients=0 and no signal at all. Still not fatal, so
  configurations carrying a stray key keep working.
- Skip targets that parse but carry no scheme; url.Parse accepts almost
  any string, so "usable target" did not previously mean much.
- Release the listener when Serve exits on its own, rather than leaking
  it to a library caller.
- Document that a Server is single use.
- Wait for the bind rather than a probe connection in tests, and report
  a Start failure instead of letting it look like a slow bind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-07 19:00:58 +00:00
fix: address review findings on the server lifecycle and config loading
All checks were successful
CI / goreleaser-lint (pull_request) Successful in 3s
CI / format (pull_request) Successful in 2m15s
CI / lint (pull_request) Successful in 3m8s
CI / test (pull_request) Successful in 1m31s
CI / build (pull_request) Successful in 4m35s
Start bound the listener inside ListenAndServe on a goroutine, so a stop
that won the race found go-smtp's listener list still empty, closed
nothing, and left Accept running forever — a SIGTERM during startup
needed SIGKILL. Bind synchronously and serve the listener we own, and
close it from Stop so a direct caller gets the same guarantee.

shutdownTimeout equalled ReadTimeout, so draining an idle client (which
only drops once its read deadline expires) raced its own budget and
surfaced an intentional stop as "context deadline exceeded", which main
turned into a non-zero exit. Give the drain room and log an overrun
instead of failing.

Also:
- Validate config after decoding. TOML ignores unknown keys, so
  [[Recipient]] or Adresses previously started a server that answered
  250 OK and forwarded nothing.
- Drop cmd/sendmail's hand-rolled PLAIN client; net/smtp.PlainAuth
  already permits plaintext to a loopback host, as backend_test.go
  relies on.
- Release the signal handler once the first signal lands, so a second
  Ctrl-C aborts a slow drain.
- go.mod: express the language version, not a patch pin, so consumers
  on go1.27.0 with GOTOOLCHAIN=local still build.
- Makefile: install goreleaser like golangci-lint, so check/build work
  on a clean checkout.
- Tests: ephemeral ports throughout, no require inside an Eventually
  condition, and regression coverage for both lifecycle bugs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-07 18:30:10 +00:00
ci: use the goreleaser from the CI image instead of goreleaser-action
All checks were successful
CI / goreleaser-lint (pull_request) Successful in 3s
CI / format (pull_request) Successful in 24s
CI / test (pull_request) Successful in 1m20s
CI / lint (pull_request) Successful in 5m43s
CI / build (pull_request) Successful in 4m9s
goreleaser-action v7.1.0 made cosign signature verification of its own
download mandatory, and ci-base ships no cosign — the goreleaser-lint
job failed on the v6.4.0 -> v7.2.3 bump.

The action was redundant anyway: ci-base already installs goreleaser
from the upstream apt repo, which is what `make build` has always used.
Both workflows now call it directly, and `goreleaser check` moves behind
a `make check` target so every CI job runs a make target.

Co-authored-by: multica-agent <github@multica.ai>

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 17:36:14 +00:00
deps: upgrade Go to 1.27.1, refresh dependencies and drop gotoolkit
Replaces the gotoolkit helpers with the standard library and the
libraries they wrapped:

- gotoolkit/encoding TOML wrapper -> pelletier/go-toml/v2 directly,
  behind a new smtp2shoutrrr.LoadConfig shared by both commands.
- gotoolkit/service + gotoolkit/model.Server -> signal.NotifyContext
  and a concrete *Server whose Start shuts the listener down
  gracefully when its context is cancelled.

Also drops the unused ANONYMOUS SASL client from cmd/sendmail, bumps
alpine, golangci-lint and the CI actions, and adds coverage for config
loading and the server lifecycle.

Closes FMG-2

Co-authored-by: multica-agent <github@multica.ai>

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 17:36:14 +00:00