feat!: the framework no longer knows Prisma Cloud — extensions own their deploy containers (ADR-0038)#138
Conversation
…pplicationOf guard projectIdOf now delegates to cloudApplicationOf, and application.provision returns branchId alongside projectId — the first step toward every descriptor reading deploy identity through the application product instead of ResolvedCloudOptions directly. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…onOf, not ResolvedCloudOptions postgres.ts, compute.ts, and prisma-next.ts stop reading o.branchId directly; they read the narrowed application product instead, so exports/control.ts becomes the only remaining consumer of the env-fed id. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Every manually-constructed ctx.application stub now carries branchId alongside projectId, matching the real application product shape; the provision-hook assertions and isCloudApplication negative cases cover the new field. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…acity)
ExtensionDescriptor gains an optional container: ContainerDescriptor
(ensure/locate/remove/deserialize), core threads the resolved
ContainerInstance through LowerContext, PreflightInput, and TeardownInput
in place of the projectId/branchId fields those inputs carried. A new
container-transport.ts owns the parent-child env-var naming and the
serialize/deserialize round trip; core reads it back once per lowering via
deserializeContainers(config.extensions, process.env) — the one place core
now legitimately touches the environment, confined to the framework's own
PRISMA_COMPOSER_CONTAINER_* namespace. PrismaAppConfig.state becomes a
StateDescriptor naming its owning extension, so resolveStateLayer can hand
that extension's own resolved container to state.create(). The container
types live in the shared-plane container-transport.ts (not app-config.ts)
because deserializeContainers only needs {id, container?}, not the full
control-plane ExtensionDescriptor/PrismaAppConfig shape — keeping the
module on the shared side of ADR-0028's plane split.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…n, drop @internal/lowering
main.ts step 7 replaces the single ensureContainers() call with a per-
extension loop over config.extensions[].container: deploy calls ensure,
destroy calls locate and throws the CLI's own platform-free "Nothing
deployed for <name>[/<stage>]" error when locate finds nothing. A new
remove loop runs after every teardown and only on a successful child exit,
preserving ADR-0034's state-before-Branch deletion order. Stage-name
validation moves to its own validate-stage.ts (verbatim from the deleted
ensure-containers.ts) and runs once, up front, before config loading.
run-alchemy.ts drops the projectId/branchId parameters for a content-blind
containerEnv map merged over the child's base env. load-config.ts's state
validation now expects a {extension, create} descriptor instead of a bare
function. The CLI package.json drops its @internal/lowering dependency —
nothing in the CLI names Prisma Cloud anymore.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…n env reads
The hosted state layer stops reading PRISMA_PROJECT_ID/PRISMA_BRANCH_ID
itself; it takes { projectId, branchId? } as a parameter, so its only
caller (the prisma-cloud target's control.ts) can supply ids read from
the resolved container instead of the environment. The exported name
changes to prismaStateLayer to leave prismaState() free for the
target's own user-facing StateDescriptor factory.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…control/preflight/teardown New container.ts transplants ensure-containers.ts's Management API logic behind the ContainerDescriptor interface: ensure() creates-if-absent, locate() finds-only (ContainerNotFoundError -> undefined, the CLI owns the not-found error), remove() branches on stage (Branch soft-delete throws; Project best-effort removal warns), and deserialize() reconstructs a PrismaCloudContainer from its own serialize() output with real narrowing. Error texts (missing workspace id/token, the Management API failure message, the Branch-delete failure, the Project-removal outcomes) are unchanged from ensure-containers.ts. control.ts's resolveOptions() drops the PRISMA_PROJECT_ID/PRISMA_BRANCH_ID reads; the descriptor gains container: containerDescriptor(); application .provision reads the resolved container via prismaCloudContainerOf(ctx .container) instead of the env-fed ResolvedCloudOptions fields. The extension id is now one shared PRISMA_CLOUD_EXTENSION_ID const, used for both id: and the new prismaState() StateDescriptor export (replacing the re-exported lowering-package prismaState). preflight.ts and teardown.ts destructure prismaCloudContainerOf(input.container) once at the top; everything downstream is unchanged. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…i->lowering exception prismaState() is now a StateDescriptor value, not a factory a config wraps in another function — state: () => prismaState() becomes state: prismaState() at all nine app-config call sites (seven examples, the website, and the integration-test fixture). architecture.config.json's crossDomainExceptions entry for cli -> lowering is deleted: the CLI no longer imports @internal/lowering at all, so the exception it used to document has nothing left to except. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ntainer as undefined Directly asserts the container-threading contract's other half — the existing test only checked ctx.application survived; this pins that LowerContext.container is undefined (not merely absent from a stub) when the extension declares no container descriptor at all. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ntainers S2-1 review finding: the module comment still referenced ensureContainers, which this branch deleted in favor of container.ts's ensure/locate. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Records the boundary move this project ships: container lifecycle (ensure/locate/remove) as an optional ExtensionDescriptor.container field, the opaque ContainerInstance/serialize()/deserialize() transport with core-owned env-var naming, StateDescriptor naming its owning extension, stage-name validation staying in the CLI, the method-bivariance soundness note ContainerDescriptor<I> shares with ServiceLowering<P, S>, and the one named exception to "core never reads the environment" (exports/deploy.ts reading back only its own PRISMA_COMPOSER_CONTAINER_* transport). Adds the index row. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…not env vars deploy-cli.md's pipeline steps 6-7 and § Stages and containers now speak only of the container descriptor and the opaque, content-blind transport (ADR-0037); Prisma Cloud's own Project/Branch mechanics become a short passage citing ADR-0023/0024/0034. Drops the stale "Id threading" bullet and the PRISMA_PROJECT_ID/PRISMA_BRANCH_ID sentence under § CLI behavior notes. core-model.md's PrismaAppConfig.state walkthrough gains StateDescriptor in place of the bare () => AlchemyStateLayer thunk. ADR-0017 and getting-started.md's config snippets drop the now-stale `() =>` wrapper around prismaState(). gotchas.md's PRO-214 entry no longer names the deleted ensure-containers.ts and points its fix reference at container.ts's current path. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ntifiers S2-2 review finding: the Consequences bullet claimed PRISMA_PROJECT_ID/ PRISMA_BRANCH_ID/ensureContainers survived outside container.ts and container-transport.ts — false, neither file mentions them. They are gone from the codebase entirely and survive only in this ADR's own historical account of what it replaced. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… plan, design) Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
wmadden
left a comment
There was a problem hiding this comment.
Looks fine. Read all the comments in this diff and evaluate whether an uninitiated reader will understand them, and whether they add value
| @@ -0,0 +1,116 @@ | |||
| /** The parent→child transport for resolved containers (ADR-0033-style opacity): the CLI parent writes each instance's `serialize()` output into one env var per extension; the alchemy child reads it back and calls the same descriptor's `deserialize`. */ | |||
There was a problem hiding this comment.
Fair — "parent→child transport (ADR-0033-style opacity)" assumed you already knew the two-process model and what ADR-0033 says. Rewritten in a9d090d: the module doc now explains it plainly (deploy runs as two processes; env vars are the only channel between them; the CLI writes each extension's serialized container into one var per extension, the alchemy process reads it back through the same extension's descriptor; the framework never reads the contents), and points at ADR-0037 — the decision that actually owns this design.
There was a problem hiding this comment.
Read all the comments in this file and evaluate whether an uninitiated reader will understand them, and whether they add value
There was a problem hiding this comment.
Swept every comment in the diff for an uninitiated reader (a9d090d). Fixed in this file: the module doc (above), "What the framework knows about the deploy target" → "The key an extension resolves a container from: which app, which stage", "Core's claim is minimal" → plain wording, and the containerEnvVarName/containerEnv docs trimmed to stop restating the code below them (kept only the example mapping). Elsewhere: container.ts no longer credits the descriptor to ADR-0017 (it's ADR-0037) and drops a stale "(spec §10)" pointer to a long-deleted spec; validate-stage.ts had two overlapping docs merged into one; app-config.ts's StateDescriptor doc says plainly why it names its extension and the container field doc points at the transport module. The comments that survived unchanged pulled their weight: the bivariance warning (prevents a real compile trap), the two-loop ordering note in main.ts (ADR-0034's state-before-Branch rule), and the point-of-use notes in deploy.ts.
…roject context Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
|
Warning Review limit reached
Next review available in: 46 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (65)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…undary Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> # Conflicts: # docs/design/90-decisions/README.md # packages/0-framework/1-core/core/src/__tests__/lowering.test.ts # packages/0-framework/1-core/core/src/exports/app-config.ts # packages/0-framework/1-core/core/src/exports/deploy.ts # packages/1-prisma-cloud/1-extensions/target/src/__tests__/invariants.test.ts # packages/1-prisma-cloud/1-extensions/target/src/exports/control.ts
commit: |
The transient workspace deletes with the project; its durable output (ADR-0038, the domain-doc rewrites) already lives in docs/ via #138. A CLI test fixture drops the last platform-flavored env-var name from framework tests. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The decision: the deploy CLI stops resolving Prisma Cloud Projects and Branches itself. Container lifecycle becomes something an extension supplies through its descriptor, and the framework orchestrates it blind:
The Prisma Cloud extension implements it (its instance carries
projectId/branchId); core holds the instance as an opaque value and hands it back to that extension's other hooks. With that, thecrossDomainExceptionsentry allowingcli → loweringis deleted andpnpm lint:depspasses without it — the framework domain genuinely imports nothing.One user-visible change rides along (breaking): the state store declaration in
prisma-composer.config.tsloses its thunk wrapper —Why
packages/0-frameworkis defined as importing nothing, but the CLI knew Prisma Cloud by name in four places:ensure-containers.tsimported@internal/loweringto resolve/delete Projects and Branches; core'sPreflightInput/TeardownInputcarried literalprojectId/branchIdfields;run-alchemy.tssetPRISMA_PROJECT_ID/PRISMA_BRANCH_IDon the alchemy child;main.tsorchestrated all of it. A lint exception held the door open, documented as known debt. While it existed, "extension-agnostic" was aspirational — no second platform extension could work. PR #113 already moved one such behavior (state-database deletion) behind ateardownhook; this PR finishes the job with the same pattern.How it works
A deploy is two processes: the CLI parent resolves containers and writes the stack file, then spawns the alchemy child, which re-imports the config from scratch. Nothing crosses that boundary except argv and env — which is why the old design threaded
PRISMA_PROJECT_ID/PRISMA_BRANCH_IDenv vars. The new design keeps the transport but makes it content-blind:ensure(deploy) orlocate(destroy;undefinedbecomes the CLI's own "Nothing deployed for<app>/<stage>" error). It passes each extension's instance back into that extension'spreflight/teardown, and — new — aremoveloop runs after all teardown hooks, which is what structurally preserves the ordering ADR-0034 needs (a stage's state database must be deleted before its Branch; the platform refuses otherwise).serialize()output into one framework-namespaced env var per extension (PRISMA_COMPOSER_CONTAINER_*). Core owns the pipe; the extension owns the payload format (the ADR-0019 principle).lower()reads the vars back, calls the same descriptor'sdeserialize, and injects the instance —ctx.containerbeside the existingctx.application, and into the state layer via the second new type,StateDescriptor({ extension, create(container) }), which names its owning extension so core knows whose container to inject. That's why the config thunk becamestate: prismaState().Extension code never reads
process.envfor deploy identity and never knows a process boundary exists — it receives its container as a parameter everywhere (the codebase's ADR-0033 rule: values are typed by the code that reads them; core claims onlyinput+serialize()).What did NOT change: the Management-API logic moved intact from the CLI's
ensure-containers.tsto the extension'scontainer.ts— same create-if-absent vs find-only split, same error texts verbatim, same best-effort production Project removal, same container-before-Alchemy ordering. Stage-name validation (git ref check) stays in the CLI as a framework contract. A prep refactor rides as the first three commits: descriptors readbranchIdfrom the application product instead of factory options, leaving exactly one extension site to swap onto the new container context.Verification
lint:deps(dependency-cruiser + architecture coverage + publishable-location + framework vocabulary) — all green with the exception deleted.@internal/loweringand noprojectId/branchId/PRISMA_PROJECT_ID/PRISMA_BRANCH_IDanywhere inpackages/0-framework/**source.examples/storefront-authagainst the dogfood workspace): Project resolved through the descriptor; the alchemy child bootstrapped hosted state from the transported container (hosted state: using state database db_… on branch br_… (ours)); the app round-trippedauth.verify() → { ok: true }; destroy deleted the state database, then removed the Project ("nothing was left in it"); a cleanup sweep found zero residue.Docs land in the same PR: ADR-0038 (the decision record), rewrites of
deploy-cli.md/core-model.md/ADR-0017's snippet/the guides, plus deletion of the previous project's.driveworkspace and addition of this one's (spec/plan/design notes — the full design rationale lives at.drive/projects/decouple-cli-from-prisma-cloud/design-notes.md).Alternatives considered (and why not)
resolve(command)operation with a mode flag instead ofensure/locate— "resolve a destroy" reads as resolving commands; two verbs state the create-vs-find split directly and let the CLI own a platform-free not-found error.containerFor(id)) instead of injection — a global lookup, and it would keep import-time identity reads in the extension factory; injection makes "ids at lowering time, not construction" structural.teardown— keeps feat(state): deploy state lives in the stage's Branch (ADR-0033) #113's hook untouched, and the CLI's two-loop order enforces state-before-Branch deletion instead of trusting each extension to remember.🤖 Generated with Claude Code