Repository navigation
Preview governance: TTL, idle teardown, caps and connection budget - #265
Conversation
🧊 Thermo-nuclear review (round 1) — REQUESTCHANGES ·
|
e37cd72 to
9387212
Compare
Require closeRunning input so the close bring-up path refreshes governance instead of wiping it; derive sweep expiry from activity plus stored durations and drop the stored-deadline branch; extract preview/admission as the single deploy admission gate over one row load; share connectionProjection/governanceStatus between deploy and sweep; remove the dead ProvisionInput.governance field; dedupe removePreview/tryRemovePreview and type the try-lock honestly; unify on one required-field GovernanceConfig from preview-env.
🧊 Thermo-nuclear review (round 2) — APPROVE ·
|
🧊 Thermo-nuclear review (round 3) — REQUESTCHANGES ·
|
Round-3 findings plus the remaining round-2 cleanups: - Single connection-budget owner: drop connections from GovernanceStatus; admission and sweep project through connectionProjection. Refresh deploys are exempt from caps and budget alike, fixing the off-by-one 429 detail. - Delete the stored expires_at column: last_activity_at plus ttl_ms/idle_ms is the single source, derived at the read surface (snapshot expiresAtForRow) and in planGovernanceExpiry via computeExpiresAtMs. Squash the two same-feature migrations. - Tombstone policy is explicit: PreviewExpiryReason types the removal cause; destroyPreviewRow always writes the same tombstone shape and keeps the seed watermark for writeProvisioningIntent to reset. - One EffectiveGovernanceMs in preview-env replaces DeployAdmission and the redeclared shapes; ProvisionInput.governanceMs is required. - GovernanceConfig is required end to end; Config composes it instead of re-declaring the fields; single canonical import path.
🧊 Thermo-nuclear review (round 4) — REQUESTCHANGES ·
|
Round-4 findings: thread last-activity watermark into the removal contract so a refreshed preview is a stale plan; fold expiry base, min-deadline and tie-break into preview-env resolvePreviewExpiry used by both the read surface and the sweep planner; make connectionProjection pure previews * perPreview with the +1 at the admission call site; delete dead SweepReason; clear expiry_reason on re-provision intent and accept; move the governed-ttl HTTP test into a focused preview-governance suite and assert full response shapes.
🧊 Thermo-nuclear review (round 5) — REQUESTCHANGES ·
|
🧊 Thermo-nuclear review (round 6) — REQUESTCHANGES ·
|
Require closeRunning input so the close bring-up path refreshes governance instead of wiping it; derive sweep expiry from activity plus stored durations and drop the stored-deadline branch; extract preview/admission as the single deploy admission gate over one row load; share connectionProjection/governanceStatus between deploy and sweep; remove the dead ProvisionInput.governance field; dedupe removePreview/tryRemovePreview and type the try-lock honestly; unify on one required-field GovernanceConfig from preview-env. # Conflicts: # apps/server/src/config.test.ts # apps/server/src/config.ts # apps/server/src/http/app.ts # apps/server/src/preview/runtime.mail.test.ts
Round-3 findings plus the remaining round-2 cleanups: - Single connection-budget owner: drop connections from GovernanceStatus; admission and sweep project through connectionProjection. Refresh deploys are exempt from caps and budget alike, fixing the off-by-one 429 detail. - Delete the stored expires_at column: last_activity_at plus ttl_ms/idle_ms is the single source, derived at the read surface (snapshot expiresAtForRow) and in planGovernanceExpiry via computeExpiresAtMs. Squash the two same-feature migrations. - Tombstone policy is explicit: PreviewExpiryReason types the removal cause; destroyPreviewRow always writes the same tombstone shape and keeps the seed watermark for writeProvisioningIntent to reset. - One EffectiveGovernanceMs in preview-env replaces DeployAdmission and the redeclared shapes; ProvisionInput.governanceMs is required. - GovernanceConfig is required end to end; Config composes it instead of re-declaring the fields; single canonical import path. # Conflicts: # apps/server/src/config.ts # apps/server/src/http/auth.test.ts # apps/server/src/http/deploy.ts # apps/server/src/http/routes.ts # apps/server/src/http/test-helpers.ts
Round-4 findings: thread last-activity watermark into the removal contract so a refreshed preview is a stale plan; fold expiry base, min-deadline and tie-break into preview-env resolvePreviewExpiry used by both the read surface and the sweep planner; make connectionProjection pure previews * perPreview with the +1 at the admission call site; delete dead SweepReason; clear expiry_reason on re-provision intent and accept; move the governed-ttl HTTP test into a focused preview-governance suite and assert full response shapes.
Round-5 findings: scope the legacy SPROUT_TTL_HOURS creation-age sweep to rows with neither ttl nor idle bound so a 7d TTL is never capped by the 72h default; collapse the three SweepDeletion builders into previewDeletion; drop the write-only GovernanceFieldIssue.code (and the now-unused kind param); make SweepPreview activity/bound fields required; unify the lock queue and held-depth in one keyed registry. Truthful boot warning plus doc and example updates for the legacy fallback; realistic ttlHours in sweep tests with new governed-survives-legacy and unbound-falls-to-legacy cases. Also fix the dashboard test's createRoutes call for required governance (rebase fallout) and drop the now-unimported computeExpiresAtMs re-export.
63bf22b to
47c0b4d
Compare
🧊 Thermo-nuclear review (round 7) — REQUESTCHANGES ·
|
🧊 Thermo-nuclear review (round 8) — REQUESTCHANGES ·
|
🧊 Thermo-nuclear review (round 9) — REQUESTCHANGES ·
|
🧊 Thermo-nuclear review (round 10) — REQUESTCHANGES ·
|
🧊 Thermo-nuclear review (round 11) — REQUESTCHANGES ·
|
Require closeRunning input so the close bring-up path refreshes governance instead of wiping it; derive sweep expiry from activity plus stored durations and drop the stored-deadline branch; extract preview/admission as the single deploy admission gate over one row load; share connectionProjection/governanceStatus between deploy and sweep; remove the dead ProvisionInput.governance field; dedupe removePreview/tryRemovePreview and type the try-lock honestly; unify on one required-field GovernanceConfig from preview-env.
Round-3 findings plus the remaining round-2 cleanups: - Single connection-budget owner: drop connections from GovernanceStatus; admission and sweep project through connectionProjection. Refresh deploys are exempt from caps and budget alike, fixing the off-by-one 429 detail. - Delete the stored expires_at column: last_activity_at plus ttl_ms/idle_ms is the single source, derived at the read surface (snapshot expiresAtForRow) and in planGovernanceExpiry via computeExpiresAtMs. Squash the two same-feature migrations. - Tombstone policy is explicit: PreviewExpiryReason types the removal cause; destroyPreviewRow always writes the same tombstone shape and keeps the seed watermark for writeProvisioningIntent to reset. - One EffectiveGovernanceMs in preview-env replaces DeployAdmission and the redeclared shapes; ProvisionInput.governanceMs is required. - GovernanceConfig is required end to end; Config composes it instead of re-declaring the fields; single canonical import path.
Round-4 findings: thread last-activity watermark into the removal contract so a refreshed preview is a stale plan; fold expiry base, min-deadline and tie-break into preview-env resolvePreviewExpiry used by both the read surface and the sweep planner; make connectionProjection pure previews * perPreview with the +1 at the admission call site; delete dead SweepReason; clear expiry_reason on re-provision intent and accept; move the governed-ttl HTTP test into a focused preview-governance suite and assert full response shapes.
Restore config.ts loader truncated by the rebase; fix app.ts syntax. Move server-only governance resolution out of @sprout/preview-env (parseGateway*, resolveEffectiveGovernanceMs, resolvePreviewExpiry) into apps/server preview/governance; preview-env keeps wire grammar plus the shared duration parser. Single violationMessage presenter for admission 429s and sweep logs; budget counts counted previews. Drop unused parse kind/issue codes. Compute legacyTtlMs once in loadConfig and thread it through route/sweep deps; make sweep governance fields required.
Complete the round-9 boundary move: violationMessage as the single violation presenter (pure-data violations, unified budget wording), parseDurationMs shared from preview-env, legacyTtlMs computed once in loadConfig and carried on LifecycleDeps instead of ProvisionInput.
Round-10 findings: - legacy creation-age bound now applies only to rows that never completed a governed deploy (lastActivityMs null), so explicit off (activity recorded with null bounds) is truly unbounded on both the read surface and the sweep; docs updated to match - getPreview reads legacyTtlMs from LifecycleDeps instead of a second parameter; routes no longer re-spreads/re-aliases it - shared governanceConfig/offGovernanceConfig fixture replaces the copied six-field GovernanceConfig literals in governance tests - 429 wire code carried on GovernanceViolation; admission emits violation.code with no re-switch on kind
Nest governance policy on Config instead of the intersection, so passing the whole Config never silently satisfies a GovernanceConfig (app and sweep pass config.governance explicitly; LiveSweepDeps names GovernanceConfig instead of indexing SweepPorts). Type legacyTtlMs as number through LifecycleDeps, RouteDeps, SweepPorts and the snapshot/sweep derivations: the only production source is SPROUT_TTL_HOURS, always a number. The null arm leaves resolvePreviewExpiry with the governed/never-governed gate only. presentListedPreview reuses the snapshot it just computed instead of re-deriving expiry fields, keeping the one-derivation invariant. evaluateGovernance runs one candidate loop for pending/settled per-repo checks and drops the redundant budget null check. Share testConfig(overrides) for full Config literals alongside the existing governanceConfig() fixtures.
47c0b4d to
c2a599e
Compare
🧊 Thermo-nuclear review (round 12) — REQUESTCHANGES ·
|
Round-12 leftovers: gateway env parsers move into config.ts (foundation no longer imports the preview domain; caps share parsePositiveInt), parseExtraGitlabHosts reuses splitListEntries, GovernanceManifest drops the server-dead raw arm to parsed ttlMs/idleMs, sweep names its lock choice with two functions and documents snapshot-at-deploy bounds, evaluateGovernance collapses the budget guard. Parser tests move to config.test.ts with loadConfig governance coverage.
🧊 Thermo-nuclear review (round 13) — REQUESTCHANGES ·
|
Model the connection budget as one pair object with fail-fast boot on a half-configured pair; share one row-expiry derivation between the read surface and the sweep; add CLI governance tests
🧊 Thermo-nuclear review (round 14) — APPROVE ·
|
Closes #241
Why the change
Previews lived until PR close with no bound, so this PR makes the gateway enforce TTL, idle teardown, per-repo/global caps, and a Postgres connection budget while keeping existing deployments unchanged apart from a loud unbounded boot warning.
Special things to note
off(unbounded, safe direction — never deletes more), while the self-serve compose stack shipsSPROUT_PREVIEW_TTL=7dso new operators are safe without configuring anything.docs/previews.md.apps/server/src/config.tsuntouched in shape for Install telemetry: the published image reports anonymous usage and deploy outcomes, on by default #261); rebased onorigin/mainbefore pushing; sweep knob staysSPROUT_SWEEP_CRON(Bun.cron, neversetInterval).Change outline
Stored deadlines drive sweep expiry through the normal teardown path.
Verified serially, one command at a time:
bun test(1078 pass),bun run typecheck,bun run test:guards(110 pass),bun run docs:check.