Closes the third file Sprint 8 deferred. Sprint 8a (commit 3f1344e8)
shipped the pure-mechanical relocation of wire.go (helpers + adapter
types). Sprint 8b crosses the behavior-change boundary: extracts an
inline block from main()'s body into a new function, which introduces
a new function call frame.
What moved
==========
cmd/server/migrations.go (new, 209 lines incl. BSL header + Phase 9
doc-comment + 6 imports + 2 functions)
Two unexported helpers:
- parseMigrateOnlyFlag() bool — hand-parses os.Args[1:] for the
`--migrate-only` token. Six-line implementation; matches the
pre-Sprint-8b inline behavior exactly (bare match, no value form,
no env override). Hand-parsed (not flag.Parse) for the same
reason the original was: keeps flag.Parse's global state out of
package main so future imports stay clean.
- runBootMigrations(cfg, db, logger, migrateOnly) bool — owns the
Phase 4 DEPL-M1 migration-via-hook posture. Reads
CERTCTL_MIGRATIONS_VIA_HOOK, gates RunMigrations + RunSeed,
handles the --migrate-only early-exit signal, runs RunDemoSeed
when CERTCTL_DEMO_SEED=true.
Returns true ONLY when migrateOnly was set; caller (main)
handles the clean exit via `return` so deferred cleanup runs.
Returns false in every other case — caller continues normal boot.
On any migration / seed error: os.Exit(1) inline (matches the
pre-extraction shape; recovery is impossible at this boot stage).
main.go delta
=============
- Lines 54-72 (the --migrate-only flag parse + its Phase 4
doc-comment): replaced with a single call
`migrateOnly := parseMigrateOnlyFlag()` plus a 6-line pointer
to migrations.go.
- Lines 178-259 (the migrations-via-hook + RunMigrations +
RunSeed + --migrate-only early-exit + RunDemoSeed inline
block): replaced with a single call
`if exitAfterMigrations := runBootMigrations(cfg, db, logger,
migrateOnly); exitAfterMigrations { return }` plus an 8-line
pointer to migrations.go.
- No imports needed adjusting in main.go — the moved code's
imports (database/sql, strings) were ALSO used by the rest of
main(); they stay. (Notably, this is unlike Sprint 8a, which
surfaced 5 unused imports requiring removal.)
main.go LOC: 2347 → 2260 (-87 lines)
Behavior-change contract (the single intentional shift)
========================================================
Every error path inside runBootMigrations calls os.Exit(1) directly
— byte-for-byte equivalent to the original inline shape (same log
message, same exit code, same no-defer-run on fatal).
THE ONE BEHAVIOR CHANGE: the --migrate-only SUCCESS path now returns
to main() rather than calling os.Exit(0) inline. Observable effect:
the `defer db.Close()` registered at line 175 in main() now runs at
clean exit instead of being skipped.
Why this is strictly an improvement (not a regression):
- The original os.Exit(0) skipped every registered defer. db.Close
never ran; the OS reclaimed the socket when the process died.
- The new `return` causes db.Close to run on the orderly main()
teardown path. PostgreSQL connection released cleanly via the
Go *sql.DB.Close() contract rather than mid-flight socket
teardown.
- Migrations + seed are SYNCHRONOUS — by the time runBootMigrations
returns true, all SQL work has fsync'd or returned errors. There's
no async work that db.Close could truncate.
- The exit code stays 0 (Kubernetes Job lifecycle still reports
success).
- The exit log message ("--migrate-only: migrations + seed
complete; exiting without starting server lifecycle") fires
BEFORE the return, identical to the pre-extraction position.
If an operator's monitoring is wired to detect "did the --migrate-only
container clean-shutdown its DB connection or did it just die," they
will see the new behavior. Every other observable signal is identical.
Documented in migrations.go's doc-comment so the next maintainer
doesn't think the change was accidental.
Why this is a separate commit from Sprint 8a
============================================
Sprint 8a was pure mechanical relocation — function definitions
moved between sibling files in the same package, zero runtime
semantics changed. Sprint 8b introduces a new function call frame,
which has a non-zero (if small + documented + improvement-shaped)
behavior delta.
Splitting these into two commits means git bisect against a future
boot-time regression gets a clean answer:
3f1344e8 ... wire.go — could not have changed behavior
<this> ... migrations.go — one specific documented shift, see
commit body + migrations.go header
Anyone tracing a boot-time issue knows EXACTLY which commit to scrutinize.
Verification (all clean):
go build ./cmd/server/... → clean (no unused imports)
go vet ./cmd/server/... → clean
gofmt -l cmd/server/ → clean
go test ./cmd/server/... -count=1 -short → ok (0.39s; main_test.go
+ the existing
preflight_*_test.go +
finalhandler_test.go +
auth_*_test.go +
tls_test.go all pass —
including main_test.go
which exercises the
boot flow through the
new call site)
staticcheck ./cmd/server/... → clean
grep -nE 'migrateOnly|migrationsViaHook|RunMigrations|RunSeed|RunDemoSeed'
cmd/server/main.go → just the runBootMigrations call site +
the parseMigrateOnlyFlag call site;
the inline block is gone.
LOC delta:
main.go: 2347 → 2260 (-87 lines: -18 from flag-parse
extraction, -75 from
migration-block extraction,
+6 from new call-site +
pointer comments)
migrations.go: new, 209 lines (incl. ~95-line Phase 9 doc-comment +
BSL header + package decl + 6-line
import block)
Phase 9 Sprint 8 closure
========================
Sprint 8a (wire.go) + Sprint 8b (this commit) together close the
Phase 9 prompt's three-file split for cmd/server/main.go:
cmd/server/main.go 2966 → 2260 (-706 lines, -23.8%)
cmd/server/wire.go new, 758 LOC
cmd/server/migrations.go new, 209 LOC
Cumulative Phase 9 (Sprints 1-8b):
config.go: 3403 → 1342 LOC (-60.6% across 7 sprints)
cmd/server/main.go: 2966 → 2260 LOC (-23.8% across this
sprint + Sprint 8a)
Combined LOC reduction in the two largest backend files: -2,767
Next queued (Sprint 9): internal/service/acme.go (1965 LOC). Per
the operator's decision after Sprint 8 (Option B = sibling files
in the same package, no subpackage split): the cut will keep the
package name `service` and split into
internal/service/{acme,acme_orders,acme_authz,acme_challenges,
acme_nonces,acme_gc}.go. Zero import-path churn for callers.
Closes: cowork/certctl-architecture-diligence-audit.html#fix-ARCH-M2
(partial — Sprint 8 fully closed at 9 of 12 effective splits)