The codefly Postgres service agent (agent.codefly.yaml: kind: codefly:service, name: postgres) — a plugin binary the codefly CLI loads,
plus the runtime image and client libraries it ships. It owns the Postgres
primitive: the Builder surface that renders a bootstrap image and kustomize
output, the Runtime surface that brings a postmaster up under docker or nix,
migrations, the schema plan, and the role/grant control plane.
It does not own the agent framework, the protobufs, the resource model or
the reusable CI workflows — those are codefly-dev/core, which this module
depends on. It does not own build execution or readiness orchestration: the CLI
runs the build and dials the readiness gate. It renders workload manifests and
nothing else — no GitOps transport, no reconciler, no git or GitHub client.
conformance_test.go enforces that last boundary by forbidden token, import,
binary and endpoint, so a manifest carrying argoproj.io/ fails the suite.
Fleet standard — handbook#68. They land hard here: this repo ships a database, and most of what it guards is invisible from a healthy boot. A half-applied migration, a role never granted, a tag left on an older digest — each of those serves traffic and is still wrong.
- Prove reuse before you build. The default answer is that the platform already has it and your change fills a gap or adds a helper on it, so the PR body names what you reused and what you deliberately did not. A second mechanism for a job one seam already does is refused however clean it is. Two tells you are reinventing: what you are writing re-answers a question an existing seam answered, and your reason for its placement is that a convenient loop was already there rather than that the responsibility belongs to it.
- Build the simplest thing that works; size is the signal. A coding task's budget is about 500 hand-written lines — generated output does not count, tests count apart — and past 2,000 the change has become a second system, so stop and ask the user instead of finishing it. Two tells, checkable before the code exists: you cannot name the one responsibility the change adds, and it is growing to make the end goal pass rather than to put a behaviour where it belongs. Wanting it to work licenses no means to get there.
- A gap in the tooling is a bug in the tooling — never a reason to reach around it. When the CLI cannot express what a composition needs, the answer is the capability fixed in whichever tool owns it, named in the PR. It is never a hand-written environment or a pinned port — not as a "workaround", not "just this once", not "until the verb lands".
- Never hack. Provide the best fix, even when it spans repos. The fix living
in
codefly-dev/coreorcodefly-dev/cliis not a reason to work around it here. Open the PR there and consume the reviewed result. When it genuinely cannot be fixed now, the deliverable is a precise issue against that owner plus an explicitly labelled stopgap — never an unlabelled one. - Classify every change that makes something work, in the PR body: a fix at the place that owns the behaviour, or a hack. A hack does not become a fix by working, by being small, by being local, or by the real fix belonging elsewhere.
- Never hardcode what the system resolves — injected environment, derived ports, service addresses, credentials copied out of another component. A runtime missing a credential can skip registration silently, so the service boots, serves, and is simply absent. Typing the value encodes something true on one machine for ten minutes and buries the defect that caused it.
- Diagnose, do not pattern-match. "It started working when I set X" is not a diagnosis — set X back and confirm it breaks. Do not trust an error message before checking its claim.
- Say what you did not verify. Unverified is not working. The container tests skip on a host with no docker daemon, and a skipped test is green.
Derived from .github/workflows/ci.yml and the reusable
codefly-dev/core/.github/workflows/go-service-ci.yml it pins by SHA.
go build ./...
go vet ./...
go mod tidy -diff # CI fails on any diff
go test ./... -skip '^(TestCreateToRunDocker|TestDirtyMigrationFailsClosed|TestLifecycleContractDocker|TestMigrationLockBudgetExpiresWhilePeerHoldsTheLock|TestMigrationStatementBudgetExpiresAndPreservesTheLedger)$' -timeout 20mThat skip list is CI's unit step, and it is the inventory of what needs a container. Everything else is template rendering, lock parsing, workflow assertions and unit tests. The three migration regressions run after it, against a locally built image:
docker build --build-arg SOURCE_DATE_EPOCH=0 --tag service-postgres:test .
SERVICE_POSTGRES_TEST_IMAGE=service-postgres:test go test . \
-run '^(TestDirtyMigrationFailsClosed|TestMigrationLockBudgetExpiresWhilePeerHoldsTheLock|TestMigrationStatementBudgetExpiresAndPreservesTheLedger)$' -timeout 20mTestLifecycleContractDocker runs later still, against the published image.
TestCreateToRunDocker runs in no ci.yml step at all — but the release
workflow runs a plain go test -v ./... with no skip list, so every excluded
test gates the release. A test that only fails there fails late.
Two prerequisite gates behave differently, and the difference is the point:
SERVICE_POSTGRES_TEST_IMAGEunset → a missing docker daemon skips; set → the same condition fails. CI sets it so the regression cannot quietly stop enforcing anything while the build stays green.requireDockerPlatformskips a missing arm64 emulator locally, butFatals whenCIis set — an arm64 archive checksum is only really checked there.
A clean local run therefore proves less than a green CI run. Say which you did.
Two further suites own their own module or fixture:
python3 scripts/qualify-runtime-library.py # libs/go standalone
POSTGRES_TEST_IMAGE=postgres@sha256:… python3 scripts/qualify-controlplane.pycd libs/go && go test ./... is green with no docker and no database: its
real-database tests sit behind the controlplaneintegration tag and DSN
variables only the qualify script supplies.
| Path | Owns |
|---|---|
main.go |
plugin entrypoint, the Settings YAML contract, credentials |
builder.go |
Builder RPCs; staging bootstrap/ and rendering its Dockerfile |
runtime.go |
Runtime RPCs; postmaster lifecycle and the state-ownership contract |
migrations.go |
one golang-migrate lineage per source, budgets, fail-closed |
schema.go, schemaplan.go |
prerequisite resolution into bootstrap/plan.json |
runtime_access.go |
roles, grants, RLS, password vs external-identity |
nixpg.go |
the docker-free backend, from the embedded nix/flake.*: one server per run |
forward.go, runtime_native.go |
a service's endpoint and database on its run's native server |
hotreload.go |
forward-only migration reload |
templates/ |
everything embedded into the binary; factory/ is what Create scaffolds |
libs/go |
separate module: the Go client library, no core, no parent |
libs/rust |
the Rust parity boundary for libs/go |
cmd/managed-bootstrap |
the explicit migration-owner command |
contracts/workload-attachment |
the sealed attachment envelope (v1, v2) |
- No runtime operation deletes data.
Stop,keep-runningandDestroyall retain database state; the contract is the table inruntime.goaboveStop. Keep it when adding a lifecycle path. - A dirty lineage fails closed —
InitandStarterror, readiness never succeeds, and nothing is repaired automatically (docs/dirty-migrations.md). - Hot reload is forward-only. An edit at or below the applied version is reported, never re-applied.
libs/gois consumed by published version, neverreplace. The qualify script fails on areplacedirective or any core/parent import.- Bootstrap migrate is source-built from the maintained parent module; see
docs/bootstrap-migrate.md. Inspect the binary graph, not justgo.mod. - The two image locks move by different mechanisms.
bootstrap-image.jsonis written by a script;runtime-image.jsonis hand-edited and only verified. Neither is a value to guess — see the skills below. - A pull request never moves the shared runtime tag. It publishes by digest;
only a push to
maintags. - CI's own shape is under test.
workflow_test.goasserts step order and env, so editing.github/workflows/ci.ymlwithout the test fails the suite. - Migrations and role reconciliation share one advisory-locked session —
build_bootstrap_lock_test.gopins that order.
Loaded on demand rather than carried here — .claude/skills/:
runtime-image-relock— the Dockerfile or one of its pins changed, and the digest inruntime-image.jsonhas to move with it.bootstrap-image-lock— a build fails "unable to select packages", or the weekly freshness check reports drift.standalone-runtime-module— a change toucheslibs/go, alone or together with the parent module.
- Branch and PR; never commit to
main. Conventional Commits for the title. - Release flow:
docs/releases.md. Keep dev tags publishing when repinning core; only normal releases use its default-branch reachability gate. - The PR body carries the fix-or-hack classification and what you did not verify. Both are rules above; answer them there rather than omitting them.
agentcontext_test.goholds this file's length budget and each skill's frontmatter contract, and runs undergo test ./....- Keep this file under ~150 lines (hard cap 200). Push depth into a nested
AGENTS.mdbeside what it describes, or into.claude/skills/. CLAUDE.mdis a pointer to this file. Keep one canonical source.- Treat this file as code: the PR that changes a process updates it.