SCRUM-218 — Config: publish a release to a channel¶
Plan ref: CFG-B8 (docs/11-admin-plane-plan.md). Stacked on SCRUM-217.
Behaviour — POST /api/admin/config/channels/{ch}/releases¶
live_ops for dev/staging; admin for live (literal route, enforced by both gateway_dev and Config's route table). Body:
{"base_release_id": 41, "versions": [{"namespace":"balance.weapons","version":11}],
"packs": [{"sha256":"…"}], "min_client_version": "1.4.0", "message": "nerf the sword"}
One transaction:
1. SELECT … FROM channel_head … FOR UPDATE — serialises publishes per channel.
2. head ≠ base_release_id → 409 stale_release "channel dev has moved from
release X to Y" (you publish exactly the diff you previewed).
3. Resolve versions and packs; unknown → 404 naming it; a server-audience
namespace → 400 "namespace … is server-only and cannot be published to a client
channel" (rejected, never silently dropped).
4. Sizes come from the blob files on disk (what clients download); a missing blob is
a server fault → 500.
5. nextval for the release id (the manifest contains it), build the manifest
(doc 02 §4: format, channel, release_id, created_at, min_client_version, config
map, packs sorted), schema.Canonical, sha256.
6. Insert the release, move the head, audit release.publish, pg_notify('config_release', ch)
— delivered only after commit.
201 {release_id, channel, manifest_sha256, min_client_version, message, created_by, created_at, manifest}.
Config ↔ Patch hash parity: the test asserts manifest_sha256 ==
sha256(schema.Canonical(manifest::text)), the exact check Patch performs after
reading jsonb. Checked during review that Canonical also normalises string
escapes (< → <), which jsonb's text output does not preserve.
How to verify¶
cd services/config
export CONFIG_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/config_test?sslmode=disable'
go vet ./... && go test -race -count=1 ./...
go test -count=1 -v -run 'Publish|Release' ./internal/...
| Test | Proves |
|---|---|
TestPublishHappyPath |
version + pack in the manifest with blob sizes; sha parity with the jsonb re-derivation; head moved; one audit row |
TestPublishStaleReleaseWritesNothing |
409; head and release table unchanged |
TestPublishIsSerialised, TestPublishConcurrentThroughTheLiveServer |
two concurrent publishes with one base → exactly one 201, one 409 |
TestPublishRejectsServerNamespace |
400 naming it |
TestPublishUnknownTargets |
unknown namespace / version / pack → 404 |
TestPublishMissingBlobIsAServerFault |
500 |
TestPublishNotifiesOnlyAfterCommit |
a LISTENer receives dev after commit, and nothing for a failed (stale) publish |
TestReleasesRoutesThroughTheLiveServer, TestLivePublishIsAdminOnly |
live_ops → 403 on live; validation → 400 |
Tests publish to dev under a session advisory lock and restore the original
head (checked: dev/staging/live back on 1/2/3). They also move dev, as Patch's
watcher tests (SCRUM-225) do — don't run the two suites against one database at the
same moment.
Results at time of writing¶
go vet,go test -race ./...against Postgres 16: pass (8 packages).
How it was built¶
DeepSeek run scoped (Landlock) to services/config (276 s, ~45k output tokens).
Claude review: transaction order read; string-escape parity probed. No changes needed.