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
Collaborator

Closes FMG-2

Upgrades Go and every dependency to their latest revisions, and removes the gotoolkit dependency in favour of the standard library and the libraries it was wrapping.

Supersedes #7 — identical tree, recreated under the butterrobot account.

gotoolkit removal

  • encoding.NewTOMLEncoding() — a thin wrapper over pelletier/go-toml/v2. Now used directly, behind a new smtp2shoutrrr.LoadConfig(path) shared by both commands. Previously both cmd/smtp2shoutrrr and cmd/sendmail duplicated the open/decode/SetDefaults sequence, and neither closed the config file.
  • model.Server — a three-method interface with a single implementation. NewSMTPServer now returns a concrete *Server, and the unused IsEnabled() is gone.
  • service.NewService(...).Start()/WaitStop() — replaced by signal.NotifyContext in main. Server.Start(ctx) now owns its own lifecycle: it serves until the context is cancelled, then calls Shutdown with a 10s deadline. Behaviour matches the old service, without the indirection.

Version bumps

from to
Go 1.25.6 1.27.1
emersion/go-smtp 0.24.0 0.25.0
stretchr/testify 1.9.0 1.12.1
pelletier/go-toml/v2 2.2.3 (indirect) 2.4.3 (direct)
x/crypto/x509roots/fallback 2025-02-14 2026-09-02
alpine (Containerfile) 3.20 3.24
golangci-lint v2.8.0 v2.13.2
actions/checkout, actions/setup-go v6 v7

Indirects also refreshed (x/net 0.25.0 → 0.58.0, x/sys, x/tools, fatih/color, go-cmp, logr). containrrr/shoutrrr and emersion/go-sasl were already at their latest published revisions.

Dropping goreleaser-action

Bumping actions/goreleaser-action v6.4.0 → v7.2.3 broke the goreleaser-lint job: v7.1.0 made cosign signature verification of the action's own goreleaser download mandatory, and ci-base ships no cosign.

Rather than pin the action back, this drops it. ci-base already installs goreleaser from the upstream apt repo — that is what make build has always used — so the action was downloading a second copy of a tool the image already had. Both workflows now invoke it directly, and goreleaser check moves behind a make check target, so every CI job runs a make target like the rest of the pipeline.

Tests

Coverage 63.1% → 75.9%. New tests cover LoadConfig (parsing, defaults, missing file, invalid TOML) and the replacement lifecycle — that a cancelled context shuts the server down and releases the listener, and that a bind failure propagates out of Start.

On this exact tree (as PR #7) CI passed format, lint, test and goreleaser-lint. Locally, against Go 1.27.1: make format (clean diff), make check, make ci-lint (0 issues), make test, make build (6 targets). The release binary was also smoke-tested end to end: mail accepted, forwarded to a shoutrrr generic target, and SIGTERM shut it down cleanly with exit 0.

Note: release.yml only runs on tags, so its goreleaser step is not exercised by this PR. The env vars it depends on (GITEA_TOKEN, GORELEASER_FORCE_TOKEN) are read by goreleaser itself rather than by the action, so invoking the binary directly is equivalent.

Also

Removed the unused ANONYMOUS SASL client from cmd/sendmail — its only caller was a commented-out line, and it was the last thing pulling go-sasl into that command.

Closes FMG-2 Upgrades Go and every dependency to their latest revisions, and removes the `gotoolkit` dependency in favour of the standard library and the libraries it was wrapping. Supersedes #7 — identical tree, recreated under the `butterrobot` account. ## gotoolkit removal - **`encoding.NewTOMLEncoding()`** — a thin wrapper over `pelletier/go-toml/v2`. Now used directly, behind a new `smtp2shoutrrr.LoadConfig(path)` shared by both commands. Previously both `cmd/smtp2shoutrrr` and `cmd/sendmail` duplicated the open/decode/`SetDefaults` sequence, and neither closed the config file. - **`model.Server`** — a three-method interface with a single implementation. `NewSMTPServer` now returns a concrete `*Server`, and the unused `IsEnabled()` is gone. - **`service.NewService(...).Start()/WaitStop()`** — replaced by `signal.NotifyContext` in `main`. `Server.Start(ctx)` now owns its own lifecycle: it serves until the context is cancelled, then calls `Shutdown` with a 10s deadline. Behaviour matches the old service, without the indirection. ## Version bumps | | from | to | |---|---|---| | Go | 1.25.6 | 1.27.1 | | `emersion/go-smtp` | 0.24.0 | 0.25.0 | | `stretchr/testify` | 1.9.0 | 1.12.1 | | `pelletier/go-toml/v2` | 2.2.3 (indirect) | 2.4.3 (direct) | | `x/crypto/x509roots/fallback` | 2025-02-14 | 2026-09-02 | | alpine (Containerfile) | 3.20 | 3.24 | | golangci-lint | v2.8.0 | v2.13.2 | | `actions/checkout`, `actions/setup-go` | v6 | v7 | Indirects also refreshed (`x/net` 0.25.0 → 0.58.0, `x/sys`, `x/tools`, `fatih/color`, `go-cmp`, `logr`). `containrrr/shoutrrr` and `emersion/go-sasl` were already at their latest published revisions. ## Dropping `goreleaser-action` Bumping `actions/goreleaser-action` v6.4.0 → v7.2.3 broke the `goreleaser-lint` job: v7.1.0 made cosign signature verification of the action's own goreleaser download mandatory, and `ci-base` ships no cosign. Rather than pin the action back, this drops it. `ci-base` already installs goreleaser from the upstream apt repo — that is what `make build` has always used — so the action was downloading a second copy of a tool the image already had. Both workflows now invoke it directly, and `goreleaser check` moves behind a `make check` target, so every CI job runs a make target like the rest of the pipeline. ## Tests Coverage 63.1% → 75.9%. New tests cover `LoadConfig` (parsing, defaults, missing file, invalid TOML) and the replacement lifecycle — that a cancelled context shuts the server down and releases the listener, and that a bind failure propagates out of `Start`. On this exact tree (as PR #7) CI passed `format`, `lint`, `test` and `goreleaser-lint`. Locally, against Go 1.27.1: `make format` (clean diff), `make check`, `make ci-lint` (0 issues), `make test`, `make build` (6 targets). The release binary was also smoke-tested end to end: mail accepted, forwarded to a shoutrrr generic target, and `SIGTERM` shut it down cleanly with exit 0. Note: `release.yml` only runs on tags, so its goreleaser step is not exercised by this PR. The env vars it depends on (`GITEA_TOKEN`, `GORELEASER_FORCE_TOKEN`) are read by goreleaser itself rather than by the action, so invoking the binary directly is equivalent. ## Also Removed the unused `ANONYMOUS` SASL client from `cmd/sendmail` — its only caller was a commented-out line, and it was the last thing pulling `go-sasl` into that command.
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>
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
953d033f46
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>
fmartingr approved these changes 2026-09-07 19:41:50 +02:00
Dismissed
fmartingr approved these changes 2026-09-07 19:51:36 +02:00
Dismissed
fmartingr approved these changes 2026-09-07 19:53:16 +02:00
Dismissed
fmartingr left a comment

Let's merge this.

Let's merge this.
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
91b5e7b77e
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>
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
45410d3de7
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>
fmartingr approved these changes 2026-09-07 21:26:06 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
fmartingr/smtp2shoutrrr!8
No description provided.