feat(orchestrate): provision saga phase — per-project Postgres role+db (D8) — M2 capstone - #73
Merged
Merged
Conversation
…b (D8) — M2 capstone Wires the long-deferred provision phase into the up saga. Per-project data isolation on the shared Postgres (spec 03, DECISIONS D8): each active project that `uses: workspace.shared.postgres` gets its own login role + owned database, created idempotently via the existing `internal/provision` (existence-guarded pgx SQL). Determinism-safe host port: provisioning runs pgx FROM THE HOST, so the shared Postgres needs a reachable port. Rather than publish it in the deterministic, golden-asserted generated compose (which would also flip the "no host ports by default" posture for everyone), the shared phase writes an UP-TIME overlay (`.devstack/shared/compose.provision.yaml`) mapping `127.0.0.1:<ledger port>:5432` and brings the shared stack up with `-f compose.yaml -f compose.provision.yaml`. `generate` output is untouched (CI determinism still byte-identical) and the port is loopback-only. `docker.Compose` gained `Overrides` for the extra `-f`. Flow: shared (postgres healthy, port published) → provision (pgx-connect to 127.0.0.1:<port> as the template admin, EnsureProject per project, record role+db ownership in the ledger under the flock) → per-project compose-up. The host port is ledger-allocated (`Manager.FreeHostPort`, purpose `pg-provision`) and re-derived idempotently by the provision phase, so it survives re-runs/crashes; the shared fingerprint folds in the provisioned set so adding a consumer re-publishes. Dev-credential default: the per-project password is the project name — a predictable credential for a loopback-only, network-isolated dev DB (THREAT-MODEL: container isolation is a non-goal), so nothing secret is generated/stored and an app opts in via `postgres://<project>:<project>@shared-postgres:5432/<project>` (never auto-overriding the app's own DB config). `--no-provision` opts out. Tested daemon-free via an injectable `PgConnect` seam: the full saga provisions app's role+db (CREATE ROLE/DATABASE on the create path), connects on loopback, records ownership, brings shared up WITH the overlay (loopback-bound to 5432), and `--no-provision` skips the phase entirely. `make ci` + `make determinism` green. Unblocks X3 firstRun (provision scope_key now exists). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Wires the long-deferred provision phase into the up saga — the M2 capstone. Each active project that
uses: workspace.shared.postgresgets its own login role + owned database, created idempotently viainternal/provision(existence-guarded pgx SQL, DECISIONS D8). Unblocks X3 firstRun.The host-port / determinism solution
Provisioning runs pgx from the host, so shared Postgres needs a reachable port. Instead of publishing it in the deterministic, golden-asserted generated compose (which would also flip "no host ports by default" for everyone), the shared phase writes an up-time overlay
.devstack/shared/compose.provision.yamlmapping127.0.0.1:<ledger port>:5432and brings the stack up with-f compose.yaml -f compose.provision.yaml.generateoutput is untouched →make determinismstill byte-identical ✅docker.Composegained anOverridesfield for the extra-fFlow
shared(postgres healthy, port published) →provision(pgx-connect to127.0.0.1:<port>as the template admin,EnsureProjectper project, record role+db ownership in the ledger under the flock) → per-projectcompose-up.The host port is ledger-allocated (
Manager.FreeHostPort, purposepg-provision) and re-derived idempotently by the provision phase (survives re-runs/crashes); the shared fingerprint folds in the provisioned set so adding a consumer re-publishes the port.Credential default (non-overriding)
Per-project password = the project name — a predictable credential for a loopback-only, network-isolated dev DB (THREAT-MODEL: container isolation is a non-goal). Nothing secret is generated or stored; an app opts in via
postgres://<project>:<project>@shared-postgres:5432/<project>and devstack never overrides an app's own DB env.--no-provisionopts out.Tests (daemon-free via an injectable
PgConnectseam)Full saga provisions
app's role+db (CREATE ROLE/DATABASE create path), connects on loopback, records ownership in the ledger, brings shared up with the overlay (loopback-bound to 5432);--no-provisionskips the phase.make ci+make determinism+ integration-build green.Spec acceptance (spec 03 / D8)
🤖 Generated with Claude Code