feat: deploy a stage as an isolated Prisma Cloud Branch (ADR-0022/0023) - #42
Conversation
60e357f to
acd50a4
Compare
@prisma/alchemy
@prisma/app
@prisma/app-assemble
@prisma/app-cli
@prisma/app-cloud
@prisma/app-nextjs
@prisma/app-node
@prisma/app-rpc
prisma-app
commit: |
wmadden
left a comment
There was a problem hiding this comment.
Preemptively approved. Fix comments first
There was a problem hiding this comment.
This deserves to be documented in the domains/ documentation. We're introducing important, long-lived concepts: a stage (which equates to a Branch on PDP) and in the Alchemy domain, we're altering its state store logic.
There was a problem hiding this comment.
Documented in 4a9d533: 10-domains/deploy-cli.md gains the container-resolution pipeline step and a "Stages and containers" section (explicit-destroy contract included); 03-domain-model/glossary.md gains Stage and Container — Project/Branch entries; the Alchemy-domain facts landed in 05-prisma-cloud/alchemy-lowering.md (Branch-per-stage placement, the resolveContainer/deleteBranch client and its reuse of the state-bootstrap idiom, and state keying unchanged — Project = stack, Branch = stage); two stale claims in pdp-data-model.md ("only ever touches the production branch", "future stages story") corrected. This ADR is now ADR-0024. On "altering its state store logic": the state store itself is unchanged by this slice — @prisma/alchemy gained the sibling container-resolution client, which shares the store bootstrap's client and adopt-oldest idiom; the docs say exactly that.
| result = { id: observed.data.id, name: observed.data.name }; | ||
| } else { | ||
| const created = yield* call(() => | ||
| client.POST('/v1/projects/{projectId}/databases', { |
There was a problem hiding this comment.
Why aren't we using the Prisma management API client/sdk?
There was a problem hiding this comment.
We are — client here is @prisma/management-api-sdk's createManagementApiClient (its typed openapi-fetch surface), constructed once from PRISMA_SERVICE_TOKEN in packages/alchemy/src/client.ts and provided to every provider through the ManagementClient service. The client.GET/POST/PATCH('/v1/…', { params }) calls are that SDK's calling convention (path-template + typed params), not hand-rolled HTTP.
| env: input.env ?? process.env, | ||
| env: { | ||
| ...(input.env ?? process.env), | ||
| PRISMA_PROJECT_ID: input.projectId, |
There was a problem hiding this comment.
This looks like Prisma CLoud config leaking into the framework
There was a problem hiding this comment.
Agreed — the PRISMA_PROJECT_ID/PRISMA_BRANCH_ID names here (and the resolveContainer import in ensure-containers.ts) are Prisma Cloud specifics living in the otherwise target-agnostic CLI. It is the accepted MVP shape of ADR-0024's "CLI ensures containers" decision. Recorded the durable fix as a Drive deferred item in this PR (68d7d77, .drive/deferred.md): a deploy-plane hook on the extension descriptor — the extension resolves its own containers and supplies the child env; the CLI stays generic.
There was a problem hiding this comment.
Both these ADRs are walls of text, impenetrable to the poor reader
There was a problem hiding this comment.
Rewritten in 7a05ac0 (and its pair in the same commit). Both ADRs now open with a grounding example — this one the system() snippet plus the Workspace → Project → Branch tree it lowers to — lead with the decision, and end with alternatives. Cut as transient: the "current mapping / remaining work" pre-implementation snapshot and the Open Questions section (every question in it had been answered by the companion ADR). ~40% shorter. Note: renumbered to ADR-0023 — main took 0022 (data-deps contract) while this was in review.
…age-as-branch spec/plan Renumbered off current main (my prior 0016/0017 collided with main-side ADRs added since the fork). ADR-0018: a Prisma App is one Project, a Stage is a Branch. ADR-0019: a stage is a deploy-time environment; the CLI resolves the Project (by root-system name) and Branch outside Alchemy. Project-id override reuses the prisma-app.config.ts from ADR-0017. Plus the fully-pinned slice spec and 4-dispatch plan for the first implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Adds resolveContainer() to @prisma/alchemy: given a workspace id, app name, and optional stage, it finds-or-creates the app's Project (adopt-oldest among duplicate names, per ADR-0019/ADR-0018) and, for a named stage, finds-or-creates its Branch by gitName. The default stage creates no Branch. Both use the existing ManagementClient; Branch create-if-absent is idempotent client-side (observe, then re-observe on a racing 409) since the Management API has no ifExists parameter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
D1 found the Management API has no ifExists field on POST branches: it accepts only gitName + isDefault and 409s on a duplicate. Spec §4 and plan D1 assumed server-side create-or-return. Rewrite both to the real mechanism (observe via GET ?gitName= exact-match, POST when absent, re-observe on a racing 409) and record the falsified assumption in design-decisions.md. Outcome unchanged; only the mechanism was mis-specified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
alchemy destroy re-evaluates the same stack, so once D3 makes projectId required the destroy child needs the ids too. Pin: both deploy and destroy inject PRISMA_PROJECT_ID/PRISMA_BRANCH_ID; destroy resolves find-only (resolveContainer gains ensure:boolean) and never creates a container; a missing container is a clear "nothing deployed" error. Named-stage Branch soft-delete (DELETE /v1/branches/:id after members are gone) is pinned to D4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…c §10) A bare `destroy` defaulting to production is a footgun: an omitted or typo'd --stage could tear down prod. Require an explicit target — --stage <name> for a branch, or --production for the project-level env; bare destroy and --stage+--production both error; --production is destroy-only. deploy is unchanged. Internals unchanged: --production is just stage=undefined find-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Before running the Alchemy stack, the CLI now resolves the app's Project and (for a named stage) Branch via the Management API, then injects PRISMA_PROJECT_ID/PRISMA_BRANCH_ID on the alchemy child for both deploy and destroy. Deploy creates-if-absent; destroy resolves find-only and fails clearly if nothing was deployed. Also enforces spec section 10's explicit-destroy-target rule (added mid-implementation): destroy no longer silently defaults to production — it requires --stage <name> or --production, mutually exclusive, with --production destroy-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…d at lowering time Grounding D3 against @prisma/management-api-sdk falsified two spec pins: (1) the db/compute create bodies do NOT accept branchId — attachment is a PATCH after create (EnvironmentVariable does take branchId+class in its body); (2) the parent evaluates prismaCloud() at config-load before ensureContainers computes the ids, so projectId cannot be required at construction — it is read at lowering time in application.provision. Rewrote spec §2/§5/§6/§7, logged decision #2, split D3 into D3a (providers) + D3b (target). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Adds an optional branchId prop to both providers. When set, reconcile PATCHes the resource onto that Branch after observe-or-create, on every reconcile, so a named-stage deploy is self-healing and idempotent. When unset (default/production stage), no PATCH is issued — production behavior is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
application.provision now references PRISMA_PROJECT_ID (set by the CLI) instead of minting a Project. When PRISMA_BRANCH_ID is set (a named stage), the Database, ComputeService, and every EnvironmentVariable it writes carry that branchId, and env vars switch to class: preview. Default-stage behavior is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… (D3 review) The D3 Opus review verified against the SDK that the compute-service create body accepts branchId (line 8628); only the database create lacks it. Code is unchanged — both providers use the uniform PATCH mechanism (a harmless extra idempotent call for compute). Record Connection/Deployment inherit the branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
D4a: after a successful alchemy destroy on a named stage, the CLI now calls DELETE /v1/branches/{branchId} to soft-delete that stage's Branch. The production environment has no branch and is never touched; a failed destroy or a deploy never triggers the delete.
Adds deleteBranch to @prisma/alchemy (tolerates a 404), deleteStageBranch to app-cli (surfaces API refusals as CliError), and wires it into main.ts's step-9 alchemy block via a new RunDeps.deleteBranch seam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
A live deploy hit compute_service:already_exists on branch "main": a project-scoped create always lands on the default Branch, and compute-service names are unique per Branch, so it collided with the same-named production service before the PATCH ran. ComputeService now puts branchId in the create body and drops the PATCH entirely; Database is unaffected (its create body has no branchId, so it stays create-then-PATCH). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Main advanced to include ADR-0018/0019/0020/0021 (config-schema decisions), so the App=Project / Stage=Branch ADRs renumbered from 0018/0019 to 0022/0023. Fixes the spec references to match the renamed ADR files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Ground each ADR in a concrete example (the system() snippet and the CLI invocations + a two-phase deploy diagram), lead with the decision, end with alternatives. Remove transient content: pre-implementation snapshots, refactor archaeology, deferred/roadmap notes, and open questions ADR-0023 already answered. Fix drift vs shipped behavior (no ownership marker, default stage creates no Branch, class is computed mechanically) and record the explicit destroy-target contract, which was missing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Fourth collision: origin/main gained ADR-0022 since the last rebase. App=Project becomes ADR-0023, stage→environment becomes ADR-0024; all cross-refs updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…iew) The CLI hard-codes Prisma Cloud specifics (resolveContainer import, the PRISMA_PROJECT_ID/PRISMA_BRANCH_ID child env) — target config leaking into the target-agnostic CLI. Accepted for this slice per ADR-0024; durable fix is a deploy-plane hook on the extension descriptor. From the run-alchemy.ts review comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Document the stage-as-branch surface: the deploy CLI's stage/container resolution pipeline and explicit destroy contract in deploy-cli.md, the Stage/Container glossary terms, and the Project/Branch container-resolution client plus unchanged state keying in alchemy-lowering.md and pdp-data-model.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
acd50a4 to
4a9d533
Compare
…production CI destroy-guard.sh used the old bare `destroy` form; the CLI now requires an explicit target, so it errors. Point it at --production, since CI deploys the production environment of a per-run stack name. The pipeline resolved containers (a cloud mutation) before assembly (local validation), so a deploy that could not assemble still created a Project/ Branch. Reordered so assembly runs first — local validation before cloud mutation — fixing the integration test that pins the "no built entry at" error contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Adds the user-facing skill for the prisma-app deploy CLI: deploy to production or a named --stage environment, and the destroy command's requirement to name an explicit target (--stage or --production). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
destroy requires an explicit target; the package script still used the bare form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Long-lived doc: state the destroy contract as present-tense fact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Main landed stage-as-branch (#42): the CLI resolves containers (Project + optional stage Branch) and injects PRISMA_PROJECT_ID/PRISMA_BRANCH_ID; the extension's application hook references that Project instead of minting one, and branch-scoped resources (Database, ComputeService, EnvironmentVariable) carry branchId with env vars classed preview on a stage / production otherwise. destroy now requires an explicit --stage/--production target. The one content conflict was packages/app-cloud/src/control.ts: main changed the inline lowerings while this branch had extracted them to src/controls/* (review R3). Resolution keeps the extracted structure and carries main's semantics into it — resolveOptions reads projectId/branchId into ResolvedCloudOptions (controls/shared.ts, with the required-at-provision check staying in the application hook), the Database lowerings (controls/postgres.ts AND controls/prisma-next.ts — the pn lowering gets the same branch scoping) and ComputeService/serialize (controls/compute.ts) gain the branchId/preview handling. control-lowering.test.ts auto-merged clean: main's stage assertions and this branch's PgWarm stubs coexist (32 pass). pnpm-lock.yaml regenerated via pnpm install. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
What
prisma-app deploy --stage <name>now provisions the whole topology into an isolated Branch of the app's single Prisma Cloud Project — its own compute, database, config, and Alchemy state — so an app can run production plus staging plus per-PR previews side by side.Implements ADR-0022 (a Prisma App is one Project; a Stage is a Branch) and ADR-0023 (a stage is a deploy-time environment; the CLI resolves Project + Branch outside Alchemy before the stack runs).
Model
--stage→ the project-level production environment (unchanged).--stage X→ an isolated BranchX.PRISMA_PROJECT_ID/PRISMA_BRANCH_IDinto the alchemy child; the target then provisions resources within them. Neither container is an Alchemy resource (a Branch can't live inside the state namespace named after it; the Project is shared across all stages).class = branchId ? 'preview' : 'production', mechanical — never a role lookup.Changes
@prisma/alchemy—resolveContainer(adopt-oldest-by-name Project resolution + create-if-absent Branch,ensureflag for find-only destroy);deleteBranch;Database/ComputeServicegainbranchId(Database attaches viaPATCH, ComputeService via the create body — see note).@prisma/app-cli— resolves + injects the ids for deploy and destroy;destroyrequires an explicit target (--stage <name>or--production; a baredestroyerrors, so an omitted stage can never silently tear down production); git-ref stage-name validation; soft-deletes the named-stage Branch after teardown.@prisma/app-cloud— the target references the injected Project instead of minting one, and threadsbranchId/classthrough every resource.Proven live
Deployed
examples/storefront-authagainst a real workspace:deploy(production)mainbranchdeploy --stage stagingstagingbranch — own compute + own empty Postgresauth.verify(){ok:true}(reaching its own DB)class:preview+branchIddestroy --stage staging{ok:true}destroy --productionmainbranch survivesThe live test also caught a real bug missed by unit tests + code review: compute-service names are unique per Branch, so a project-scoped create collided with production on
main— fixed by creating on the Branch via the create body (design-decision #3).Note
Supersedes #37 — that PR's ADR-0016 is the same "App=Project, Stage=Branch" decision, renumbered here to ADR-0022 (main independently took ADR-0016–0021 after that branch forked). #37 is being closed as superseded.
🤖 Generated with Claude Code