SCRUM-219 — Config: release history, rollback, promote, audit feed¶
Plan ref: CFG-B9 (docs/11-admin-plane-plan.md). Stacked on SCRUM-218. Completes
the Config API surface in doc 02 §5.
Routes¶
| Route | Role | Behaviour |
|---|---|---|
GET /channels/{ch}/releases?before=&limit= |
viewer | {head_release_id, releases:[{release_id, manifest_sha256, min_client_version, message, created_by, created_at, is_head}], next_before} newest first |
POST /channels/{ch}/rollback {release_id, base_release_id} |
admin | head locked; stale base → 409 stale_release; release not of this channel → 404; already the head → 409 no_changes. Moves the pointer only — no new release row. Audit release.rollback {from,to}, pg_notify |
POST /channels/{ch}/promote?from=<ch> {base_release_id, message?} |
admin | the ladder is dev → staging → live; from must be the channel directly below (live?from=staging, staging?from=dev), else 400. Head locked, source head share-locked; builds a new release for ch with the source's config/packs/min_client_version (shared builder with Publish); identical to the current head → 409 no_changes. Audit release.promote, pg_notify |
GET /audit?cursor=&limit=&action=&actor=&from=&to= |
viewer | {entries:[{id, at, actor_id, actor_name, source:"config", action, target, details}], next_cursor} — the fixed shape the Dashboard merges; cursor is opaque base64url of {"before":id} |
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 'Rollback|Promote|ListReleases|ListAudit|ReleaseOps|Audit' ./internal/...
| Test | Proves |
|---|---|
store.TestListReleasesPagingHeadAndIsHead |
paging, is_head, head_release_id |
store.TestRollbackMovesPointerAuditsAndNotifies |
publish A then B on dev, roll back to A: head A, no new release row, one audit, NOTIFY dev; foreign release 404; to the head 409; stale base 409 |
store.TestPromoteCopiesContentAndNotifies |
staging?from=dev: new release, channel:"staging", new id, identical config/packs, sha parity with the jsonb re-derivation; again → 409 no_changes; NOTIFY staging |
store.TestListAuditFiltersAndPaging |
ordering, action/actor/time filters, cursor paging |
api.Test{ListReleases,Rollback,PromoteLadder,PromoteBody,Audit}Validation, TestAuditCursor* |
400 cases incl. live?from=dev, malformed cursor |
server.TestReleaseOpsThroughTheLiveServer |
the whole flow over HTTP with real JWTs; live_ops 403 on rollback/promote |
Tests mutate only dev/staging (not live), under advisory locks, restoring heads (checked afterwards: dev/staging/live back on 1/2/3).
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 (318 s, ~59k output tokens).
Claude review: Rollback/Promote transactions read, suite run, head restoration
checked. No changes needed.