Extract pkg/controls into a standalone go/controls module¶
- Authors
- Matt Cockayne, Claude Opus 4.8 (AI drafting assistant)
- Date
- 2026-07-13
- Status
- IMPLEMENTED (2026-07-13)
Summary¶
pkg/controls is a standalone service-lifecycle supervisor: startup ordering,
health monitoring (liveness/readiness), graceful shutdown with a bounded timeout,
OS-signal handling, and a self-healing restart policy. It has clear reuse value
for any long-running Go service, independent of the rest of GTB.
This spec extracts it to gitlab.com/phpboyscout/go/controls following the
module-extraction playbook (§5
per-component procedure, §10 findings from the chat extraction). Unlike chat,
controls is already framework-free: production pkg/controls imports zero
go-tool-base packages, its only non-stdlib dependency is github.com/cockroachdb/errors,
and its only logging seam is a nil-safe *slog.Logger (default slog.DiscardHandler).
There is no config.Containable coupling and no *FromProps adapter to leave
behind. This is therefore the simplest kind of extraction — a git mv of the
source plus a direct caller repoint — markedly less involved than chat (no
per-provider modules, no vendor SDKs, no security-scanner surprises, no facade).
The pre-extraction lifecycle hardening (D8 startup cancel race, D9 error-send guard) is already complete on this branch, alongside the earlier D1–D7 supervisor hardening (2026-06-12-controls-supervisor-lifecycle.md). The package is race-clean at 96.6% coverage. Nothing further blocks the move.
Motivation¶
- Reuse: the supervisor is genuinely general-purpose; framework-free packaging lets any service adopt it without pulling in GTB.
- Unblocks the transport stack:
pkg/http,pkg/grpc, andpkg/gatewayall build oncontrols. Extractingcontrolsfirst gives that stack a stable external foundation to sit on when it is extracted later (per the extraction report's roadmap). It is the recommended next substantial pick post-chat. - Low risk, high signal: because the package is already decoupled, this extraction exercises the playbook procedure on a clean core with almost no decoupling work — a good, cheap confidence-builder before the chunkier transport-stack and VCS-tree extractions.
Target module¶
- Module path:
gitlab.com/phpboyscout/go/controls(bare name in thegosubgroup, per naming convention and playbook §2). - Package name: stays
controls. - Docs microsite:
controls.go.phpboyscout.uk(core module ⇒ full Diátaxis microsite, likesigning/chat; not README-only). - Local dir:
~/workspace/phpboyscout/go/controls(mirrors the subgroup). - Reference scaffold: the freshest sibling,
~/workspace/phpboyscout/go/chat(cicd componentsv0.22.0, go1.26.5).
What moves vs. what stays¶
Moves into go/controls (the whole production package + pure tests)¶
Production source (all framework-free today):
controller.go,services.go,healthcheck.go,controls.go(types:Controller,Service,RestartPolicy,HealthCheck,HealthReport,ServiceStatus,ServiceInfo,State, theRunner/Controllable/HealthCheckReporter/… interfaces, allWith*options),doc.go.README.mdcontent → seeds the microsite.
Pure tests (move as-is, with a one-line seam swap — see below):
controls_test.go,example_test.go,healthcheck_test.go,healthcheck_cancel_internal_test.go,lifecycle_test.go,lifecycle_race_test.go.
These reach 96.6% coverage on their own, comfortably clearing the ≥90% bar, so the module is self-sufficient for coverage without any transport-coupled tests.
Test seam swap: the pure tests currently obtain a logger via
logger.ToSlog(logger.NewNoop()) and (in one place) logger.NewCharm(buf) for
log-capture assertions. In the module these become stdlib-only:
slog.New(slog.DiscardHandler) and a small slog text/JSON handler writing to a
bytes.Buffer. This removes the last pkg/logger reference and tests the real
seam (*slog.Logger) directly.
Stays in GTB (transport-coupled integration tests)¶
Three integration tests drive real pkg/grpc + pkg/http servers through a
controller (internal/testutil, mocks/pkg/config, pkg/grpc, pkg/http):
controls_integration_test.go,server_integration_test.go,shutdown_integration_test.go.
They test GTB's composition of controls with its transport servers, not the
supervisor in isolation, so they cannot live in the framework-free module (it
would create a forbidden go-tool-base dependency and a cycle). They stay in GTB,
repointed to the go/controls import path. Open question O1 covers their new
physical home, since pkg/controls/ is deleted.
The GTB e2e controls scenarios (features/, test/e2e/support/controller.go,
test/e2e/steps/controls_steps_test.go) likewise stay in GTB — they exercise the
CLI/server wiring, a GTB concern.
Consumer surface & cut-over strategy¶
GTB consumers of pkg/controls (≈10 production files, ≈79 references across ≈17
symbols): pkg/http (server.go, config_adapter.go), pkg/grpc (server.go,
config_adapter.go), pkg/gateway (gateway.go, config_adapter.go),
pkg/telemetry (observability.go, observability_config_adapter.go),
test/e2e/support/controller.go, and one generated mock
(mocks/pkg/grpc/healthSource.go).
Per playbook §10.8 this surface is moderate and alias-free-repointable — there
are no GTB-owned constants and no *FromProps adapter to preserve. The cut-over is
therefore a direct repoint (like signing), not a facade (as chat
needed):
- Change every
gitlab.com/phpboyscout/go-tool-base/pkg/controlsimport togitlab.com/phpboyscout/go/controls. Symbol references are unchanged (package name stayscontrols). - Delete the in-tree
pkg/controls/package. - Hand-edit the one generated mock's import (per the
just mockschurn note — do not regenerate the whole tree). - No
reexport.go, noconstants.go, no adapter helpers, no coverage backfill (there is no adapter package to re-cover — §10.9 does not apply).
Repository framework (playbook §3)¶
Bootstrap from the go/chat scaffold:
- CI (
.gitlab-ci.yml):go-lint,go-test(enable_e2e: false— see O2),go-security,releaser-pleaser,zensical-pages,renovate-self— all@v0.22.0. Nogoreleaser(library, no binaries). .golangci.yamlv2, local prefixgitlab.com/phpboyscout/go/controls, GTB linter set (perfsprint/wrapcheck/wsldisabled).depfootprint_test.go: assertgo list -deps ./...excludesgo-tool-base,viper,pflag,charmbracelet, OpenTelemetry, and all cloud SDKs. (Confirmed achievable now: the only external dep iscockroachdb/errors.).mockery.ymlfor the exported interfaces (HealthCheckReporter,Runner,Controllable, etc.) if downstream mocking is wanted.- ≥90% coverage (96.6% today).
cockroachdb/errorsthroughout;*slog.Loggerthe only logging seam.LICENSE,CHANGELOG.md(releaser-pleaser-owned), README with the toolkit badge,requirements-lock.txtfor the zensical build.
Documentation (playbook §4)¶
- Author the
controls.go.phpboyscout.ukDiátaxis microsite by porting GTB's existing controls docs (docs/explanation/components/controls*, plus the packageREADME.md) and de-GTB-ing them (dropProps, reference the option API and*slog.Loggerseam). Reference → pkg.go.dev. - GitLab Pages custom domain + Cloudflare DNS (CNAME + verification TXT), mirroring
chat/signing. - GTB side (§4.1): replace the GTB controls component page with a stub linking to
the microsite; migration note in
docs/reference/migration/; components index + README badge updated.
Migration procedure (playbook §5)¶
- Bootstrap
phpboyscout/go/controlsrepo to §3; trivial-commit CI green; Pages domain reserved. git mvthe production source + pure tests; apply the test seam swap.- Add
depfootprint_test.go; regenerate mocks; confirm ≥90%; race/lint/vet green. - Author the microsite; wire
zensical-pages; enable the custom domain + DNS. - Cut
v0.1.0via the Release-MR flow (Matt merges; releaser-pleaser tags). - GTB cut-over (single MR): add the
go/controls v0.1.0dependency; deletepkg/controls/; repoint all imports (no aliases); relocate the three transport-coupled integration tests (O1); hand-edit the mock import; runjust ci; add the migration note. - GTB docs cross-reference the microsite (§4.1).
- Hand-off (not repoint): downstream consumers (
haileys-appand others outside this workspace) repoint themselves on their own schedule, guided by the GTB migration note — we do not edit their repos (O3).
Resolved decisions (review 2026-07-13)¶
- O1 — Home for the transport-coupled integration tests. Resolved: a new
dedicated
test/integration/controls/package holdscontrols_integration_test.go/server_integration_test.go/shutdown_integration_test.go, keeping the controls-lifecycle integration story cohesive in one place (repointed togo/controls+ still importing GTB'spkg/grpc/pkg/http). - O2 — Godog BDD in the module. Resolved: no — unit tests + runnable
Exampletests suffice forv0.1.0(as withsigning);enable_e2e: false. GTB retains the user-facing e2e controls scenarios. - O3 — Downstream consumers. Resolved: there are multiple downstream
consumers of
pkg/controlsbeyond this workspace (e.g.haileys-app, and others not visible here). We do not repoint them; the extraction is a clean pre-1.0 break and each consumer repoints itself, guided by the GTB migration note (import path…/go-tool-base/pkg/controls→…/go/controls, symbols unchanged). Step 8 is therefore a hand-off via the migration guide, not edits to downstream repos. - O4 — cicd component version. Resolved: pin
v0.22.0, matching thechatsibling for lockstep; bump all Go modules together later.
Acceptance criteria¶
go/controlsbuilds and resolves undergitlab.com/phpboyscout/go/controls;depfootprint_test.gopasses (no go-tool-base / framework / SDK deps); ≥90% coverage; race/lint/vet green;v0.1.0published and proxy-resolvable.- GTB consumes
go/controls v0.1.0;pkg/controls/deleted; all callers repointed; the relocated integration tests pass;just cigreen (modulo the known-unrelatedinternal/agentbuildvcs sandbox test). - Docs live at
controls.go.phpboyscout.uk; GTB controls page is a stub linking out; migration note added; extraction report checklist tickscontrols.