Skip to content

feat!: release v3.0.0 - #66

Merged
jasoet merged 103 commits into
mainfrom
next
Aug 18, 2026
Merged

feat!: release v3.0.0#66
jasoet merged 103 commits into
mainfrom
next

Conversation

@jasoet

@jasoet jasoet commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Promotes the v3 line to main, publishing v3.0.0.

⚠️ Merge with a merge commit, not squash

gh pr merge <N> --merge     # correct
gh pr merge <N> --squash    # WRONG

Measured, by simulating both locally and running semantic-release --dry-run:

Strategy Surviving BREAKING CHANGE footers Computed version
--merge 22 3.0.0
--squash 0 2.14.0

A v2.14.0 tag on a module whose path is /v3 is not installable, and it's only visible once published. This is documented in MAINTAINING.md with the recipe to re-check.

Expected CI result

The API compatibility check reports informationally, not blockingci.yml keys that off head_ref == 'next'. gorelease reporting Inferred base version: none / Suggested version: v3.0.0 is the expected output: there is no prior non-prerelease /v3 baseline.

What's in it

102 commits, 22 with BREAKING CHANGE footers, across all 14 packages.

  • Module path → github.com/jasoet/pkg/v3
  • logging absorbed into otel
  • Functional options and a single WithOTelConfig injection point everywhere, enforced by internal/archtest
  • Third-party types de-leaked from utility packages; kept deliberately in the SDK-integration packages (ADR 0004)

MIGRATION.md is the consumer-facing record, and it covers breaks that shipped without footers and therefore never reached the generated notes — notably server's removal of signal handling, temporal.WorkflowManager.Close, and grpc's gateway base-path stripping.

Verification on next @ d802f3a

Check Result
task ci:check (unit + lint) ✅ 0 issues
go vet -tags='example integration argo' ./... ✅ clean
Full integration suite, 20 packages ✅ green
Version computation 3.0.0

After merging

Confirm the published tag is v3.0.0 and that the Go proxy warmup step succeeded. Because this promotes a prerelease line, semantic-release moves the existing v3.0.0-next.* notes onto the release channel rather than regenerating a changelog body; the release description is worth enriching with a pointer to MIGRATION.md.

jasoet added 30 commits July 22, 2026 09:52
BREAKING CHANGE: module path is now github.com/jasoet/pkg/v3; consumers must update imports.
BREAKING CHANGE: the logging package is removed. Migrate: logging.Initialize -> otel.Initialize, logging.InitializeWithFile -> otel.InitializeWithFile, logging.ContextLogger -> otel.ContextLogger, logging.LogLevel -> otel.LogLevel (identical signatures).
BREAKING CHANGE: NewConfig now takes variadic Option; With*/Disable* methods on *Config removed (use package-level options); DisableTracing/DisableMetrics renamed WithoutTracing/WithoutMetrics.
BREAKING CHANGE: LoadStringWithConfig and NestedEnvVars removed; use LoadStringWithOptions with WithEnvPrefix/WithDefaults/WithNestedEnvVars.
BREAKING CHANGE: Config builder methods removed (use retry.New with options); WithOTel renamed WithOTelConfig; Config.OperationName renamed Config.Name; invalid configs now error at Do time instead of panicking in setters.
… API

BREAKING CHANGE: MakeRequest/MakeRequestWithTrace return *rest.Response; Is* package funcs replaced by Response methods; IsUnauthorized renamed IsAuthError; RequestInfo.TraceInfo is now rest.TraceInfo.
…ty retry hook

BREAKING CHANGE: NewUnauthorizedError/NewExecutionError/NewServerError/NewResponseError/NewResourceNotFoundError and RecordRetry are no longer exported (error types remain exported for type switches).
…n tracing

BREAKING CHANGE: (*ConnectionConfig).Pool() removed; use db.NewPool(db.WithConnectionConfig(cfg)).
jasoet added 29 commits July 23, 2026 08:14
BREAKING CHANGE: ExecuteConcurrentlyTyped type parameters are now [R, T] (result first).
…APIs

- README/INSTRUCTION/AI_PATTERN/PROJECT_TEMPLATE now describe the v3 module
  (14 packages; logging merged into otel) instead of the frozen v2 world
- replace removed db APIs (ConnectionConfig.Pool, RunPostgresMigrationsWithGorm)
  with db.NewPool(WithConnectionConfig, WithOTelConfig) + RunPostgresMigrations
- fix stale /v2 import paths and coverage claims in fullstack-otel example and
  release guide
- proxy warmup targeted /v2 (silently no-op on every v3 tag) -> /v3 in both
  Taskfile and release workflow
- drop invalid -tags=!examples from test steps (negation is not valid tag syntax)
- add a go vet pass over example/integration/argo-tagged code so tagged-only
  regressions can no longer slip past CI
…events

- route URL-shaped OTLP endpoints through WithEndpointURL (bare host:port keeps
  WithEndpoint); a scheme-prefixed endpoint previously mangled the export URL and
  silently dropped all logs
- LayerContext.Error now records on the span exactly once (was emitting two
  exception events and inflating error counts)
- InitializeWithFile returns a genuinely nil io.Closer for console-only output
- apply options before building the default LoggerProvider; validate before any
  global-state mutation; add trace-correlation + severity-mapping tests

BREAKING CHANGE: WithOTLPEndpoint("") now errors instead of silently disabling
OTLP export; the nil-config zerolog fallback defaults to Info and labels the
emitter as 'scope' rather than 'service'.
…-After

- normalize request headers into a private copy before the middleware chain,
  fixing a nil-map panic (hit by the documented OTel example) and a data race
  that mutated the caller's map
- retry only idempotent methods by default (opt in with WithRetryNonIdempotent);
  stop retrying permanent errors (url/x509/context); retry 429 and honor Retry-After
- store OTel config on the client and merge after all options so option order no
  longer disables telemetry
- redact userinfo/secrets from url.full, carry true response size, fractional-ms
  durations; real in-memory exporters in OTel tests

BREAKING CHANGE: POST/PATCH are no longer retried by default; ExecutionError and
UnauthorizedError messages now include the cause/response body.
- derive the real status from the handler error after next() so errors and 404s
  are no longer recorded as 200, and set span status Error for 5xx
- fractional-millisecond request duration; install Recover() and order OTel
  middleware outermost so rejects are observed
- add WithBindAddress, tag func-typed config fields, redact sensitive query params,
  document built-in limits/timeouts
- rewrite the Struct Tags section: mapstructure (or dual) tags are required for
  keys that differ from the lowercased field name (plain yaml tags silently drop)
- anchor env-prefix matching on a trailing underscore and reject empty prefixes so
  foreign vars (APPLE_*) no longer pollute config
- use InConfig so nested env vars beat defaults; document the AutomaticEnv limitation
- ExecuteConcurrentlyTyped rejects a nil resultBuilder before execution instead of
  panicking after all work completes
- include the goroutine stack trace in recovered-panic errors
- correct three misleading claims in the example README (error timing, the
  item-dropping concurrency snippet, partial-results)
- return an error for a nil operation instead of panicking (matches the docs)
- wrap both ctx.Err() and the last operation error on cancellation so errors.Is
  finds the real failure
- do not run the operation when the context is already cancelled; add
  WithUnlimitedRetries; cover the OTel path with an in-memory exporter
- back off / exit the accept loop on persistent errors instead of spinning at 100%
  CPU on a non-shutdown listener error
- reserve a starting sentinel so concurrent Start calls cannot both proceed
- dial via net.Dialer.DialContext + ssh.NewClientConn so ctx aborts the connect
- refuse a pre-existing leaf symlink at the target (Lstat + O_NOFOLLOW), closing an
  arbitrary-file-overwrite-outside-destination hole
- open with O_TRUNC so a shorter file fully replaces a longer one
- enforce WithMaxArchiveSize mid-file (LimitReader) instead of after the write, and
  remove partial output on size/IO error; accept a relative '.' destination
…rors

- normalize in StripChecksum/ExtractChecksum and strip separators in DecodeBase32
  so a string ValidateChecksum accepts round-trips correctly instead of silently
  yielding a wrong payload
- add errors.Is-matchable sentinels (ErrEmptyInput, ErrInvalidCharacter, ErrOverflow,
  ErrValueTooLarge); correct the generator-polynomial doc; document the leading-zero
  CRC blind spot; add fuzz tests
- build the H2C http.Server with ReadTimeout/WriteTimeout=0 (keeping
  ReadHeaderTimeout/IdleTimeout) so long-running and streaming RPCs are no longer
  killed by the default 5s/10s HTTP timeouts
- extract trace context with an explicit TraceContext+Baggage propagator instead
  of the never-set global; add a stream tracing interceptor and forward traceparent
  through the gateway annotator
- order tracing before logging so access logs carry trace_id/span_id; surface an
  error when GracefulStop times out; register uptime metrics once across restarts
- per-check health timeouts, signal.Stop cleanup, rate-limit validation, and
  port-polling (not sleep) in tests
- snapshot the client and container id under RLock in every client-using method and
  return an error when closed, fixing a data race and nil panic after Close()
- remove the created container and clear state when ContainerStart fails; run wait
  and start cleanup on context.WithoutCancel so a canceled caller ctx cannot leak a
  running container
- surface in-stream image-pull errors; use WaitConditionRemoved with AutoRemove;
  derive the host from DaemonHost() so remote Docker/Podman daemons work
- tag daemon-dependent tests with //go:build integration and replace hardcoded
  ports/names and sleeps with :0 mapping, unique names, and Wait
…onns

- map the default SSLMode 'require' to a valid go-mssqldb encrypt value and validate
  MSSQL SSLMode, so an MSSQL pool connects out of the box
- quote/escape Postgres DSN values and build the MSSQL DSN via net/url, closing a
  DSN parameter-injection / TLS-downgrade hole for special-character credentials
- run migrations on a dedicated checked-out connection (postgres.WithConnection) so
  m.Close() no longer closes the caller's pool or pins a connection for process life
- default zero pool sizes, close the pool on failed NewPool, avoid duplicate metrics
  on the global provider, and round sub-second timeouts up
- pass user commands as shell fragments in MapReduce/ParallelDataProcessing/
  ParallelTestSuite (quoting only data args) so generated workflows no longer try to
  exec a program literally named e.g. 'wc -w' and fail at runtime
- apply the default retry strategy to leaf templates only (not the entrypoint or
  exit-handler steps templates, which would re-run succeeded steps)
- deep-copy builder-owned maps/slices/templates in Build so a built workflow cannot
  mutate the builder or other results
- join all builder errors and add sentinel errors (ErrWorkflowFailed, ErrWaitTimeout,
  ErrTemplateConflict, ErrNilConfig); SubmitAndWait wraps context errors and aborts on
  permanent poll failures; wire real ContinueOn/status gating into ConditionalDeploy;
  sort ParallelTestSuite output; validate nil config and empty inputs
- WorkflowManager list/count/history use the high-level client APIs so every method
  targets the client's configured namespace instead of a hardcoded 'default', and
  raw-gRPC calls no longer bypass the tracing interceptor
- job.History keys in-flight activities by scheduled-event id so concurrent
  activities are attributed to the correct step
- StartAll stops already-started workers on failure; Close is idempotent; per-worker
  registrars replace the never-evicted global dedup maps
- add WithTLS/WithCredentials/WithClientOptions (Temporal Cloud), wire schedule
  CatchupWindow and Args, honor StatsOpts.TodayOnly and HistoryOpts.MaxEvents, make
  DeleteSchedules converge, fix the logger caller skip, and replace can't-fail
  integration guards with require; fix the string(int) bug in the timer example
…pass

- add db.WithOTelConfig to the options contract registry (it was missing though the
  function exists) and assert the expected compliant-package set so a dropped
  registry entry fails instead of passing vacuously
- document grpc's sanctioned unexported-config exemption and refresh the stale
  rest ClientOption note
- fix misspellings (canceled/canceling/dialing) flagged by misspell
- drop redundant trailing newlines from fmt.Println in examples (go vet)
- wrap an over-long error string (lll); extract shouldRetryRequest from NewClient
  to bring cyclomatic complexity back under threshold
- tag docker/testutil_test.go integration so its daemon-only helpers are not
  reported unused in the default build
grpc TestServerStopDuringStartNoZombie: freePort reserves an ephemeral port then
releases it before the server binds, so under load Start can lose the race with
'address already in use'. That is infrastructure noise orthogonal to the zombie
behavior under test, so retry the iteration with fresh ports instead of failing.

temporal ExecuteSimpleWorkflow: Temporal workflow functions take workflow.Context,
not context.Context; the integration test used the latter, so the SDK miscounted
the arguments and ExecuteWorkflow failed with 'expected 2 args ... but found 1'.
The bug was previously masked by a log-and-return guard now replaced with
require.NoError.
TestStopGracefulTimeoutReturnsError's health handler blocked on <-release and
ignored its context, so the forced Stop that follows a graceful-timeout could not
cancel it: grpcServer.Stop() waited on the stuck handler and the test deadlocked
(a 10m timeout under the runner's -race timing, though it passed locally). Select
on ctx.Done() as well so the handler returns when the forced Stop cancels the RPC,
while still blocking through the graceful window so the timeout path is exercised.
#59)

## Problem

The blocking `API compatibility check` step was guarded by:

```yaml
if: github.ref_name != 'next' && github.base_ref != 'next'
```

On the eventual `next` → `main` v3 pull request, `base_ref` is `main`
and `ref_name` is the merge ref — neither equals `next`, so the
**blocking** gate fires and reports the whole intended v3 break set. The
release PR would have been unmergeable.

Verified locally against a clean tree at this commit:

```
# summary
Inferred base version: none
Suggested version: v3.0.0
```

Also, a gate that can block a merge was floating on `gorelease@latest` —
not reproducible.

## Change

- Add `github.head_ref != 'next'` to the blocking condition and `||
github.head_ref == 'next'` to the informational one, so anything
involving `next` reports without gating.
- Pin gorelease to `v0.0.0-20251113190631-e25ba8c21ef6`, the
`golang.org/x/exp` pseudo-version already in `go.mod`. Exposed as a
workflow-level `env` var so both steps stay in lockstep.
- Document the "no baseline until the first non-prerelease /v3 tag"
behaviour in a comment, since that is expected output for the release
PR.
- Fix the stale `/v2` module path in the `.golangci.yml` header comment.

Closes the first two "Open Process Items" in
`docs/plans/2026-07-22-v3-audit-backlog.md`.
## Problem

Both HTTP tracing middlewares started their server span from the raw
request context:

```go
ctx, span := tracer.Start(req.Context(), ...)   // server/otel_middleware.go
ctx, span := tracer.Start(ctx, ...)             // grpc gateway
```

No `Extract`, so an inbound `traceparent` header was discarded and
**every server span became a new root**.

This is inconsistent within the library itself:

| path | behaviour |
|---|---|
| `rest` client outbound | injects `traceparent` ✅ |
| `grpc` unary/stream interceptors | extracts ✅ |
| `server` HTTP middleware | **dropped** ❌ |
| `grpc` gateway HTTP middleware | **dropped** ❌ |

So a `rest` → `server` call made with this library on both ends rendered
as two disconnected traces.

## Change

Extract W3C trace context + baggage from request headers before starting
the span, reusing the composite propagator the gRPC interceptors already
use. With no inbound headers this is a no-op and the span is still a
root.

## Tests (written first, observed failing)

New tests assert the span joins the caller's trace. Before the fix:

```
expected: "4bf92f3577b34da6a3ce929d0e0e4736"
actual  : "a1be0badcd4d1db205624bba8acf1c48"   <- fresh root
expected: "00f067aa0ba902b7"
actual  : "0000000000000000"                   <- no parent
```

Added for both packages, plus a companion test pinning the no-header
case to a root span so the fix can't regress into always-remote-parent.

## Verification

`task ci:check` — all packages pass, golangci-lint 0 issues. `go vet
-tags='example integration argo' ./...` clean.

## Note for the migration guide

Existing users gain correctly-parented server spans. Traces that
previously appeared as separate roots will now join their caller — a
visible change in any tracing backend, in the correct direction. Closes
the "Shared gap (server + grpc)" item in
`docs/plans/2026-07-22-v3-audit-backlog.md`.
## Problem

`container.InspectResponse.NetworkSettings` is a **pointer**, and the
daemon leaves it `nil` for containers without networking
(`--network=none`, and some podman responses).

Four exported `Executor` methods dereferenced it unconditionally and
panicked:

| method | line |
|---|---|
| `MappedPort` | `network.go:86` |
| `GetAllPorts` | `network.go:147` |
| `GetNetworks` | `network.go:175` |
| `GetIPAddress` | `network.go:205,212` |

`ContainerTarget.State` (`target.go:87`) already guards this exact field
— these four were the gap. Recorded as "docker NetworkSettings parity"
in `docs/plans/2026-07-22-v3-audit-backlog.md`.

## Change

The projection logic was entangled with the client call, which is why
the nil case had never been tested — it needs a live daemon to reach.
Extracted four pure functions of an inspect response (`portBinding`,
`allPortBindings`, `networkNames`, `networkIPAddress`) and guarded
there, so the nil path is unit-testable without Docker.

Behaviour:
- `GetAllPorts` / `GetNetworks` return an empty map/slice instead of
panicking — callers range over the result, so nil would be a second
footgun.
- `MappedPort` / `GetIPAddress` return their existing not-found errors
unchanged.
- Also stopped dereferencing `nil` `EndpointSettings` values inside the
`Networks` map, which was a second latent panic.

No public signature changes.

## Tests (written first, observed failing)

New `docker/network_test.go` — the first run failed to compile
(`undefined: portBinding`, …), driving the extraction. 17 cases covering
bound/unbound/missing ports, named vs first-available network lookup,
nil `NetworkSettings`, and nil `EndpointSettings`.

These raise docker's unit coverage without needing a daemon, which
matters since the package sat at 41% precisely because most paths
require one.

## Verification

`go test ./docker/` passes; `golangci-lint run ./docker/...` 0 issues.
Full `task ci:check` green on this change.
## Why

v3 carries **22 commits with `BREAKING CHANGE` footers** and had no
consumer-facing migration document — the single largest gap blocking the
v3.0.0 release.

Worse, several breaks shipped in `fix:`-typed commits **without**
footers, so semantic-release will never surface them:

- `temporal.WorkflowManager.Close` removed entirely (commit `cadc208`'s
footer omits it)
- `rest.Client.HandleResponse` unexported (`6cc5af1`)
- `grpc.MountGatewayOnEcho` now strips the base path; `Start*` return
`nil` instead of `http.ErrServerClosed`; `GetGRPCServer()` returns nil
after `Stop`
- `server` **signal handling removed** — consumers silently lose
graceful termination, and nothing fails to compile to tell them

`MIGRATION.md` states each of these explicitly.

## Accuracy

Every signature in the guide was verified against the code, not against
the audit backlog's notes. That caught two places where the backlog
described a *planned* API that did not ship:

| backlog said | actually shipped |
|---|---|
| `temporal.NewClient` accepts/returns a ctx | `NewClient(opts
...Option) (client.Client, error)` |
| `argo` operations keep a `namespace` param | `SubmitWorkflow(ctx,
client, wf)` — no namespace |

Both are documented as built. All 14 packages covered, plus a closing
section on telemetry changes that need no code edit but will move
dashboards.

## ADRs

Five decisions a future reader would otherwise have to reverse-engineer:

| ADR | Decision |
|---|---|
| 0001 | Freeze v2, ship v3 as one big bang (module-path versioning is
what makes it cheap) |
| 0002 | OTel config is injected, never serialized — a half-deserialized
provider set is worse than none |
| 0003 | Constructor naming: `New` for the primary type, `New<Thing>`
when there are several |
| 0004 | Selective de-leak — utility packages hide their dependency,
SDK-integration packages don't |
| 0005 | `grpc` restarts, `server` doesn't; and the `http.server.*`
attribute-set divergence |

ADR 0003 resolves the "constructor naming split" backlog item as **not a
defect** — `retry.New` returns a `Config` because a `Config` *is*
retry's primary artifact. The rule already holds; renaming would have
added breaks that make call sites worse.

`CONTEXT.md` pins the vocabulary those depend on (utility vs
SDK-integration package, selective de-leak, escape hatch, convention
contract, docs-of-record).

## Also

- `README.md` claimed v2/v3 arrive "with minimal API changes".
Corrected.
- `INSTRUCTION.md` gains `MIGRATION.md`, `CONTEXT.md` and `docs/adr/` in
Key Paths, plus a note to update the guide **as breaks land** rather
than at release time.

All relative links in the new docs verified to resolve.
## Coverage figures

Stale in **both** directions, and three packages had none. Regenerated
from a real unit+integration run (`-tags=integration`, Docker-backed
testcontainers):

| package | README said | actual |
|---|---|---|
| server | 77.1% | **97.0%** |
| grpc | 71.2% | **82.0%** |
| ssh | 78.2% | **85.6%** |
| db | 76.7% | **83.5%** |
| concurrent | 95.1% | **100.0%** |
| otel | 84.8% | 89.5% |
| compress | 82.4% | 85.3% |
| temporal | 81.2% | 84.5% |
| docker | 83.1% | 84.1% |
| config | 97.6% | 96.4% |
| rest | 92.9% | 93.0% |
| **argo** | *missing* | **94.8%** |
| **retry** | *missing* | **100.0%** |
| **base32** | *missing* | **100.0%** |

The README also told readers to open `output/coverage-all.html` — no
task produces that file — and described a methodology that did not match
the task it named. Now: figures come from `task test:integration` →
`output/coverage-integration.html`; `task test:complete` additionally
runs argo (needs k8s) → `output/coverage-complete.html`.

## examples/otel was entirely pre-v3

`/v2` import path, struct-literal configs throughout, and a link to
`otel/examples/example.go` — a directory that does not exist.

The **example program itself** used `&otel.Config{...}`. That still
compiles, but `loggerProviderSet` is unexported, so a struct literal
silently skips the default zerolog logger provider that `NewConfig`
installs. The example was teaching a pattern that quietly loses logging.
Converted to functional options; verified the program still prints its
documented output (the no-op example needed an explicit
`WithoutLogging()` to stay all-false, which is exactly the trap worth
showing).

## Broken links and unrunnable instructions

- `docker/README.md` linked the deleted `logging` package and said
"OpenTelemetry v2".
- Three READMEs linked a `CONTRIBUTING.md` that was never written.
Rather than delete the promise, wrote the file — the workflow it
describes already existed, scattered across README and INSTRUCTION.
- `examples/grpc/README.md` linked `../README.md` (nonexistent
`examples/README.md`).
- `examples/compress` and `examples/ssh` gave run instructions from
`<pkg>/examples/` directories that do not exist, and omitted the
`example` build tag — so they could not have worked. Both verified with
`go build -tags=example`.

Full markdown link scan now clean except two links inside historical
`docs/plans/` records, deliberately left as point-in-time documents.

## Backlog hygiene

Split resolved from open, each with a pointer to where the fix landed.
Notably, two entries were **verified still present rather than assumed
fixed**: `ssh`'s accept-loop reads `t.stopCh` unlocked at three points
while `Start` reassigns it under `t.mu`, and `t.wg.Add` after `Accept`
still races `Close`'s `wg.Wait`. Only the busy-spin half of that item
was ever fixed. Recorded with line numbers; left open deliberately as a
v3.x item.

Two backlog items are now closed as **decisions rather than defects** —
docker's `Inspect`/`GetStats` escape hatches (ADR 0004) and the
server/grpc lifecycle divergence (ADR 0005).

## Verification

`task ci:check` green (0 lint issues), `go vet -tags='example
integration argo' ./...` clean, `CLAUDE.md` == `AGENTS.md` parity
confirmed.
…64)

## Symptom

The Release pipeline failed twice in a row on the db integration suite —
**a different test each time**, same signature:

```
--- FAIL: TestOTelCallbacksTableAndRowsAffected (10.77s)      # run 1
--- FAIL: TestPostgresMigrationsInvalidPath   (10.52s)        # run 2 (re-run)

failed to ping database at localhost:33451/testdb:
  [::1]:33451  failed to receive message: unexpected EOF
  127.0.0.1:33451  read: connection reset by peer
```

Both ~10s in — **well inside the 60s deadline**, so not a timeout. Both
pass locally. The triggering commit was docs-only.

## Root cause

The wait strategy, not the tests.

The postgres image runs `initdb` against a **temporary** server, stops
it, then starts the real one. All eight Postgres containers in this
package waited only on the mapped port:

```go
wait.ForListeningPort("5432/tcp").WithStartupTimeout(60*time.Second)
```

So the container could be declared ready while the server was on its way
down, and the connection that followed got reset.

This is documented in testcontainers-go itself —
`postgres.BasicWaitStrategies()`:

> First, we wait for the container to log readiness **twice**. This is
because it **will restart itself after the first startup**.
> […] For non-linux OSes like **Mac** […] Docker will have to start a
separate proxy. **Without this, the tests will be flaky on those OSes!**

The db tests used only the *second half* of that pair. The runner is a
Mac.

That also explains the intermittency and why a docs commit "caused" it:
the window is invisible on an idle machine and opens under CI load.
MySQL and MSSQL in the same file were never affected — they already wait
on a real query via `wait.ForSQL`, with a comment saying so.

## Fix

Add the occurrence-2 log check alongside the port check, behind one
shared helper (`postgresReady()`) so the strategy can't drift back one
call site at a time, with a deadline sized for a loaded runner. 8 call
sites across 3 files now share it.

Confirmed no other package is affected: every `postgres.Run` in the repo
is in `db/`, and the remaining `ForListeningPort` uses (Temporal, sshd)
are services that don't restart themselves during init.

## Verification

| condition | result |
|---|---|
| db integration, idle | ok — 80.3s |
| db integration, **10-way CPU contention** | ok — 102.7s |
| `task ci:check` | pass, 0 lint issues |
| `go vet -tags='example integration argo' ./...` | clean |
| `golangci-lint --build-tags=integration ./db/...` | 0 issues |

The loaded run is the condition CI was failing under.
)

## Why now

This file is a hazard for the pending v3 promotion.

It predated the v3 rework and contradicted itself — module path given as
`/v3`, but `main`'s purpose described as *"active development for
**v2.x** releases"*. It never mentioned the `next` branch or
`release/v2`, and described `ci.yml` as PR-only when it also runs on
push and tags.

The consequential part: it told authors their PR description becomes
release notes **"when squash-merged"**, unqualified.

That is correct for merges into `next`. It is wrong for promoting a line
into `main`:

```bash
gh pr merge <N> --squash   # collapses 102 commits into 1
                           # destroys all 22 BREAKING CHANGE footers
```

semantic-release would then analyse a single commit against `main`'s
last tag (`v2.13.1`) and compute **`v2.14.0`** instead of `v3.0.0` — an
invalid tag for a module whose path is `/v3`, and only visible once
published.

## Change

- Branch table: `main` / `next` / `release/v2` / `release/v1` with their
module paths, status and release patterns.
- The merge-commit rule for line promotion, with the reasoning above
stated at the point of decision so it can't be "simplified" away.
- A pre-promotion checklist, including that gorelease reporting
`Inferred base version: none` is the **expected** result on the release
PR, not a failure.
- Accurate workflow triggers, the gorelease pinning note, and why the
tagged `go vet` step exists separately from `task check`.
- Points at `CONTRIBUTING.md` for contributor-facing process rather than
duplicating it.

All relative links verified to resolve.
@jasoet
jasoet merged commit 2bc0f38 into main Aug 18, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant