From 2aafdcf6f063a71d20a70ffeef260037c1fa4461 Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 3 Nov 2026 09:24:11 +0100 Subject: [PATCH 1/2] docs(drive): design for deployment identity and node identity with consent Two Drive projects that replace prisma/composer#355. deployment-identity: a deploy finds its Project by logicalId only, refuses to create one without --create, Console creates the Project at connect, and a platform script backfills logicalId on old Projects. node-identity-and-consent: written for handoff. Every node gets a required logicalId and an optional name, data-losing deploys use the ORM's consent model, and the platform scopes CI tokens to one Project. Signed-off-by: willbot Signed-off-by: Will Madden --- .../deployment-identity/design-notes.md | 33 +++ .drive/projects/deployment-identity/plan.md | 44 ++++ .drive/projects/deployment-identity/spec.md | 63 ++++++ .../assets/logicalid-walkthrough.md | 203 +++++++++++++++++ .../assets/user-scenarios.md | 81 +++++++ .../node-identity-and-consent/design-notes.md | 207 ++++++++++++++++++ .../node-identity-and-consent/plan.md | 42 ++++ .../node-identity-and-consent/spec.md | 72 ++++++ 8 files changed, 745 insertions(+) create mode 100644 .drive/projects/deployment-identity/design-notes.md create mode 100644 .drive/projects/deployment-identity/plan.md create mode 100644 .drive/projects/deployment-identity/spec.md create mode 100644 .drive/projects/node-identity-and-consent/assets/logicalid-walkthrough.md create mode 100644 .drive/projects/node-identity-and-consent/assets/user-scenarios.md create mode 100644 .drive/projects/node-identity-and-consent/design-notes.md create mode 100644 .drive/projects/node-identity-and-consent/plan.md create mode 100644 .drive/projects/node-identity-and-consent/spec.md diff --git a/.drive/projects/deployment-identity/design-notes.md b/.drive/projects/deployment-identity/design-notes.md new file mode 100644 index 000000000..72039d479 --- /dev/null +++ b/.drive/projects/deployment-identity/design-notes.md @@ -0,0 +1,33 @@ +# Design notes — deployment-identity + +## Principles + +- A deploy's target is chosen by an identifier, never by a label. +- Nothing is created on the platform unless a human asked for it by name. +- Composer knows nothing about GitHub. Repo-to-Project knowledge lives on the platform's link record. + +## The model + +The Project's `logicalId` is the app name (ADR-0051). Container resolution matches on it and on nothing else. If no Project matches, the deploy stops unless `--create` was passed. The platform creates the Project when a repo is connected, so a connected repo's CI run always finds its Project and never needs `--create`. + +``` +prisma deploy module.ts # finds Project by logicalId, or refuses +prisma deploy module.ts --create # finds, or creates with that logicalId +``` + +The refusal message is the whole point: "No Project with logicalId `storefront` in this workspace. This deploy would create a new project rather than update one. Pass `--create` to do that. If this repository is connected in Console, the Project should already exist; check the connection." + +## Alternatives considered + +- **PR 355: Composer checks `GITHUB_REPOSITORY_ID` against the source-repository listing.** Rejected. Makes Composer GitHub-aware, re-verifies at deploy time what the platform decided at connect time, and is only a safeguard by its own admission. +- **The OIDC exchange returns the app name and the deploy action passes `--name`.** Rejected. Locks CI but hides the identity from the repo, so a developer cannot tell which Project a module file deploys to, and a local and a CI deploy of the same file can target different Projects. +- **CI passes `--create` on first deploy.** Rejected. Makes the flag meaningless in CI and couples the generated workflow to a Composer version. Creating at connect time needs no workflow change and no version coupling. +- **Keep the display-name fallback with a warning.** Rejected. The fallback is the mechanism that lets a name adopt an unrelated Project. The backfill removes the need for it. + +## Open questions + +- None at project level. + +## References + +- Full design, including the phases this project does not deliver: `.drive/projects/node-identity-and-consent/design-notes.md`. diff --git a/.drive/projects/deployment-identity/plan.md b/.drive/projects/deployment-identity/plan.md new file mode 100644 index 000000000..62caff56c --- /dev/null +++ b/.drive/projects/deployment-identity/plan.md @@ -0,0 +1,44 @@ +# Project Plan — deployment-identity + +## Summary + +Three slices: two on the control plane that can run in parallel, then one in Composer that ships only after both are in production. Linear project: [Deployment identity](https://linear.app/prisma-company/project/deployment-identity-find-the-project-by-identifier-create-only-when-3f43eb77b990) (P-TML-1156). + +**Spec:** `.drive/projects/deployment-identity/spec.md` + +The control plane work runs from the clone at `wip/pdp-control-plane` (bot identity, draft PRs per that repo's rules). The Composer work runs on this branch. + +## Slices + +### Slice 1 — Console creates the Project at connect time (TML-3579, control plane) + +The setup-PR path in `connectRepositoryForComposer` creates the Project with the reserved module name as `logicalId` and the region the user chose (already stored on the setup attempt), then creates the link with `projectId` set, the way the "connect without a setup PR" path in `project.create/action.ts` already does. `reflectProjectOnLink` becomes dead and is removed. "Remove setup" (`releasePendingLinkUnlessAdopted`) and the unmerged-PR archive path delete the Project the connect created, when it still holds nothing. The pending-link name-conflict query stops assuming `projectId` is null. + +- **Builds on:** nothing. +- **Hands to:** slice 3 — every repo connected from now on has its Project before its first deploy, so Composer can refuse to create. + +### Slice 2 — Backfill `logicalId` on linked Projects (TML-3580, control plane) + +A script under the control plane's scripts convention: for every live `ProjectScmRepo` with a `moduleName` whose Project has no `logicalId`, set it. Dry-run by default, refuses a value that is not unique in the workspace, prints the remaining count. Run against production once reviewed. + +- **Builds on:** nothing. +- **Hands to:** slice 3 — zero linked Projects without a `logicalId` in production. + +### Slice 3 — Composer finds by `logicalId` only and creates only with `--create` (TML-3581, Composer) + +`resolveProject` drops the display-name match. `ResolveContainerOptions.ensure` becomes a mode with three values: a plain deploy finds the Project or refuses, `deploy --create` finds it or creates it, and destroy finds it or reports that nothing is deployed. The mode is threaded from a new `create` option on the `deploy` operation and `prisma deploy --create` through `LocateContainerInput` and the extension's `ensureContainer`. A miss without `--create` is a structured `DEPLOY.PROJECT_NOT_FOUND` error with the message from the design notes. The decision records on stage resolution and node addresses amended, deploying guide and core-concepts skill updated, PR 355 closed. + +- **Builds on:** slices 1 and 2 in production. +- **Hands to:** close-out. + +## Sequencing + +- Parallel: slices 1 and 2. +- Stack: (1, 2) → 3. Slice 3 may be built and reviewed alongside, but is not released until the backfill has run and slice 1 is deployed. + +## Close-out (required) + +- [ ] Verify all acceptance criteria in `.drive/projects/deployment-identity/spec.md` +- [ ] Migrate long-lived docs into `docs/` (ADR amendments land with slice 3; nothing else is long-lived) +- [ ] Strip repo-wide references to `.drive/projects/deployment-identity/**` +- [ ] Delete `.drive/projects/deployment-identity/` diff --git a/.drive/projects/deployment-identity/spec.md b/.drive/projects/deployment-identity/spec.md new file mode 100644 index 000000000..893375a67 --- /dev/null +++ b/.drive/projects/deployment-identity/spec.md @@ -0,0 +1,63 @@ +# Purpose + +Make it impossible for an edit that looks cosmetic to redirect a deploy. Today the root module's name is the only thing that chooses the Project a deploy lands in, and Composer falls back to matching display names, so renaming the root in a connected repo can deploy into another repository's Project or create a stray one. After this project, a deploy finds its Project by identifier only, never creates one unless told to, and the platform has given every Project an identifier. The rename still fails, but it fails before anything is created or overwritten, with a message that says what would have happened. + +This is phase 1 of a larger design (see `node-identity-and-consent`). It solves Søren's reported problem and closes prisma/composer#355 without changing how modules are written. + +# At a glance + +Four changes, two in Composer and two on the platform: + +1. **Find by identifier only.** `resolveProject` in `packages/1-prisma-cloud/0-lowering/lowering/src/container.ts` matches a Project's `logicalId` against the app name and nothing else. The display-name fallback, oldest first, is deleted. +2. **Create only when told.** A deploy whose app name matches no Project stops with a structured error: this would create a new project, not update one; pass `--create`. `prisma deploy` gains that flag and the control API gains the matching option. Destroy is already find-only. +3. **Console creates the Project at connect.** The setup-PR path in the control plane's connect flow creates the Project with the reserved module name as its `logicalId` and links it, as the "connect without a setup PR" path already does. CI never creates a Project. Releasing or abandoning the setup deletes the empty Project it created. +4. **Backfill.** A one-time platform script sets `logicalId` on every linked Project that has none, from the link's reserved module name. + +# Non-goals + +- Changing the node API. `logicalId` and `name` as separate fields, and refusing the single-string form, are the follow-on project. +- Consent for data-losing deploys. Follow-on project. +- Reading anything GitHub-specific in Composer. The deploy action and the generated workflow are unchanged. +- Preventing a deliberate deploy into another Project with a workspace-wide token. That is scoped CI tokens, a platform follow-up. +- A deprecation period for the display-name fallback. Composer is pre-1.0 and the backfill makes it unnecessary. + +# Place in the larger world + +- Composer: container resolution, which finds or creates the Project and Branch before Alchemy runs (decision record "a stage is a deploy-time environment resolved to Project and Branch", amended by "a node's address is its logical ID on the platform"), and the `prisma deploy` command. +- Control plane: `connectRepositoryForComposer` in `services/console-compute/lib/composerConnect.server.ts`, `connectWorkspaceToScmRepo`, `createProject` and its `reflectProjectOnLink` step, which becomes unnecessary once the Project exists before the link. The GitHub Actions token exchange and `cloud-deploy-action` are untouched. +- The control plane's decision record "project slug as the deploy-target identity" made the module name the Project's `logicalId`. This project finishes that work by removing the display-name fallback it kept for older Projects. +- prisma/composer#355 is closed in favour of this project. The decision record it proposed, "GitHub project selection checks active repository connections", is not adopted. + +# Cross-cutting requirements + +- A deploy that cannot find its Project never creates, mutates or reconciles anything before it stops. +- Every repo connected through Console today keeps deploying across the change. The backfill runs before the Composer release that removes the fallback. +- The `--create` refusal is a structured CLI error with a `fix` that names the flag and, for a connected repo, says the Project should already exist and to check the Console connection. +- Composer stays unaware of GitHub. + +# Transitional-shape constraints + +- Order of landing: the platform changes (create at connect, backfill) ship first. The Composer release that removes the fallback and adds `--create` ships after the backfill has run, or the setup PR's first deploy breaks. +- Until the Composer release ships, a Project created at connect time is found by the old resolver through its `logicalId` match, so the platform change is safe to ship alone. + +# Project DoD + +- [ ] A renamed root in a connected repo fails its CI deploy with the "would create a new project" error and creates nothing. Proven against a real workspace. +- [ ] A fresh Console connect creates the Project before the setup PR's first deploy, and that deploy lands in it. +- [ ] Releasing a setup ("Remove setup") or closing the setup PR unmerged deletes the Project the connect created, and only that one. +- [ ] Every linked Project in production has a `logicalId`; the backfill reports zero remaining. +- [ ] `prisma deploy --create` creates a Project from a laptop; without the flag the same deploy refuses. +- [ ] The two Composer decision records named above are amended. The deploying guide and the core-concepts skill say that a deploy finds its Project by `logicalId` and needs `--create` to make one. The guide's `prisma deploy module.ts --name demo-42` example still works, with `--create` on its first run. +- [ ] prisma/composer#355 closed with a comment pointing at this project. + +# Open questions + +- None blocking. Whether Console should also allow editing a Project's `logicalId` by hand for Projects with no link (so a user can fix one the backfill cannot) is answered in the follow-on project if it comes up. + +# References + +- Design synthesis and scenarios: `.drive/projects/node-identity-and-consent/design-notes.md` and `assets/`. +- Slack thread with Søren, 2026-10-14, and prisma/composer#355. +- Composer decision records, in `docs/design/90-decisions/`: ADR-0006 "every node is named", ADR-0024 "a stage is a deploy-time environment resolved to Project and Branch", ADR-0050 "Composer runs as prisma deploy and prisma dev", ADR-0051 "a node's address is its logical ID on the platform". +- Control plane, prisma/pdp-control-plane: ADR-010 "a connected repository is the trust boundary for CI deploys" (it mints a workspace-wide token), ADR-011 "project slug as the deploy-target identity", `docs/CiOidcTokenExchange.md`, and `docs/compute/GitHubImport.md` for the repository link lifecycle. +- Linear: Deployment identity (Terminal team). diff --git a/.drive/projects/node-identity-and-consent/assets/logicalid-walkthrough.md b/.drive/projects/node-identity-and-consent/assets/logicalid-walkthrough.md new file mode 100644 index 000000000..96ffcfdfc --- /dev/null +++ b/.drive/projects/node-identity-and-consent/assets/logicalid-walkthrough.md @@ -0,0 +1,203 @@ +# Walkthrough: `logicalId` and `name` on every node + +A working note for the design discussion. Not a spec. + +## 1. The system we use throughout + +A small shop. One root module, a Postgres database, a web service, an API +service, and a reusable auth module that owns its own database and service. + +``` +store root module → Project +├── catalog postgres → Database +├── web compute → service +├── api compute → service +└── auth module (no row) + ├── database postgres → Database address auth.database + └── service compute → service address auth.service +``` + +## 2. Today + +```ts +export default module('store', ({ provision }) => { + const catalog = provision(postgres({ name: 'catalog', contract, config: './prisma.config.ts' })); + const auth = provision(authModule, { secrets: { signingKey: envSecret('AUTH_SIGNING_SECRET') } }); + provision(web, { deps: { db: catalog, auth: auth.rpc } }); + provision(api, { deps: { db: catalog } }); +}); +``` + +One string per node does three jobs: it is the identity (`logicalId` on the +platform row), the label (display name in Console), and the address other +nodes use. The three footguns, all caused by editing that one string: + +| Edit | What the author meant | What happens | +| --- | --- | --- | +| `catalog` → `catalogue` | fix spelling | next deploy deletes the `catalog` Database and creates an empty `catalogue` | +| `store` → `storefront` | nicer name | next deploy creates a second Project; `store` is left behind with its data | +| `store` → `billing` (another repo's project in the workspace) | copy-paste, or a "rename" | next deploy reconciles into the `billing` Project and overwrites its production stage | + +The third row is Søren's case. The first two are the same defect. + +## 3. Proposed: two fields on every node + +Every node has `logicalId` (identity, required) and `name` (label, optional). +Only `logicalId` reaches the platform row's `logicalId` and the topology. +`name` reaches the row's display name and nothing else. Omitting `name` +means Composer does not manage the label. + +```ts +export default module({ logicalId: 'store', name: 'Store' }, ({ provision }) => { + const catalog = provision(postgres({ logicalId: 'catalog', name: 'Catalog', contract, config })); + const auth = provision(authModule, { logicalId: 'auth', secrets: { signingKey: envSecret('AUTH_SIGNING_SECRET') } }); + provision(web, { logicalId: 'web', name: 'Web', deps: { db: catalog, auth: auth.rpc } }); + provision(api, { logicalId: 'api', deps: { db: catalog } }); +}); +``` + +The address is still the path of `logicalId`s: `auth.database`. Nothing +else changes in the model. Local dev, state keys, Alchemy naming, and the +topology payload all keyed off the address already. + +Same three edits, re-run: + +| Edit | What happens now | +| --- | --- | +| `name: 'Catalog'` → `'Catalogue'` | Console label changes. No structural change. | +| `name: 'Store'` → `'Storefront'` | Console label changes. Same Project. | +| `logicalId: 'store'` → `'billing'` | You changed the identity. Composer finds the `billing` Project and deploys into it. Deliberate. | + +The label edits are safe. The identity edit is still possible, and still +destructive, but it no longer looks cosmetic. That is the whole fix. + +## 4. What should the root's `logicalId` look like? + +The Console connect flow writes the root into the setup PR. Three shapes it +could generate. + +### 4a. A user-chosen slug, validated at connect (today's `moduleName`) + +```ts +export default module({ logicalId: 'store', name: 'Store' }, …) +``` + +Console asks for a name, validates the slug format and uniqueness in the +workspace, and reserves it on the link. The user sees the same word in code, +in the Console URL, and in CLI output. Looks like something a human owns. + +### 4b. An opaque platform id + +```ts +export default module({ logicalId: 'proj_cm3xk9q2a0001', name: 'Store' }, …) +``` + +Unique by construction, no reservation step, no conflict check. But it reads +as a foreign key that fell into the user's code. It is exactly what ADR-011 +rejected for committed config: when the Project is deleted, or the repo is +forked, deploys fail until someone edits a value they did not write and do +not understand. + +### 4c. A generated readable slug with a suffix + +```ts +export default module({ logicalId: 'store-7f2k', name: 'Store' }, …) +``` + +Unique without a conflict check, still readable. The suffix is the part +people will "tidy up". See 5c. + +## 5. Why a user would change the root `logicalId`, and what it feels like + +### 5a. Forking the repo to make a second, separate app + +They copy the repo, want a new Project, and must change `logicalId`. With +4a they pick `store-eu`. The first deploy says: + +``` +No Project with logicalId "store-eu" in workspace wksp_…; creating it. +``` + +That is the right experience: a new identity, stated, and the system does +what it says. With 4b they have no idea what to type. They either delete +the field and get an error, or copy another project's id from the Console +URL, which is the Søren case by a different road. + +### 5b. Moving the repo to an existing Project + +They want this repo to take over the `billing` Project. They set +`logicalId: 'billing'`. The deploy finds that Project and reconciles into it. +The experience should make the takeover visible before it is irreversible: + +``` +Project "billing" (proj_…) exists and holds a production deploy from +another source. Continue? (--yes) +``` + +Whether Composer asks, or just logs, is a separate decision. The point is +that the author typed the other project's identity on purpose. + +### 5c. "Tidying" a generated value + +With 4c, someone sees `store-7f2k`, thinks the suffix is noise, and edits it +to `store`. The next deploy creates a new empty Project and production +stops. The error that would have to catch this is "the Project you were +deploying to has a different logicalId than your code," and Composer cannot +know that without a second identity source. Generated suffixes invite the +edit the field is meant to prevent. This is the argument for 4a. + +### 5d. The honest rename + +They want the Project to be called "Storefront" everywhere. With two fields +the answer is "change `name`." The URL slug in Console, if it shows the +`logicalId`, keeps saying `store`. That is the one place the model leaks: a +stable identity that is also user-visible will eventually look stale. The +same is true of every slug-based system (GitHub repo names, Vercel project +names) and users accept it. + +### 5e. A child rename + +`logicalId: 'catalog'` → `'catalogue'` on the database. Composer plans to +delete `catalog` and create `catalogue`. Today that is silent. With the +field explicit, the plan output should name the consequence: + +``` +~ catalog (Database): logicalId changed → delete "catalog", create "catalogue". + Data in "catalog" will be lost. Use `name` to change the label instead. +``` + +That message is only possible because the two fields exist: Composer can +tell a label edit from an identity edit. + +## 6. Where the value comes from in each flow + +| Flow | Who sets the root `logicalId` | Where it lives | +| --- | --- | --- | +| Console connect, setup PR | Console reserves it on the link, writes it into `module.mjs` | the repo | +| Repo with its own Composer config | the author already wrote it; Console reads it at connect and reserves it | the repo | +| Hand-written repo, service token, no link | the author | the repo | +| Composer-created children | the author | the repo | + +In every row the value is in the repo. The link record on the platform is a +copy that lets Console validate uniqueness and attach the created Project. +Nothing is supplied at run time, so Composer stays unaware of GitHub. + +The alternative, an environment variable from the deploy action, would make +the root's identity come from the platform at run time instead of from code. +It locks CI harder, but it makes the value invisible in the repo, so a +developer reading `module.mjs` cannot tell which Project it deploys to, and a +local deploy and a CI deploy of the same file can target different Projects. +That contradicts "the graph is Composer's image." Not recommended as the +primary path; the token scope is the platform's job (ADR-010). + +## 7. Open points this note surfaces + +1. Which shape for the root value: 4a is recommended. 4b and 4c are + rejected above. +2. Whether a root `logicalId` change that lands on an existing Project with + deploys from another source should warn, confirm, or just proceed (5b). +3. The plan output for identity changes on children (5e) is a separate, + valuable slice. +4. `--name` on the CLI today overrides the root's single string. With two + fields it should become a `logicalId` override, or be removed. The + generated workflow does not use it. diff --git a/.drive/projects/node-identity-and-consent/assets/user-scenarios.md b/.drive/projects/node-identity-and-consent/assets/user-scenarios.md new file mode 100644 index 000000000..0f5ae8443 --- /dev/null +++ b/.drive/projects/node-identity-and-consent/assets/user-scenarios.md @@ -0,0 +1,81 @@ +# Deployment identity: user scenarios the design must cover + +Companion to [logicalid-walkthrough.md](logicalid-walkthrough.md). Each row +is a concrete situation, what the user expects, and which decision covers +it. "Open" marks the ones the discussion has not settled. + +Decisions so far, for reference: + +- D1. Every node has `logicalId` (identity, required, hand-written) and `name` (label, optional). +- D2. The Project is selected by `logicalId` only. The display-name fallback is removed. +- D3. Composer's graph is the source of truth; labels edited on the platform are set back on the next deploy. Omitting `name` leaves the label alone. +- D4. Creating a Project is explicit: a deploy that would create one needs `--create`. +- D5. Proposed, open: Console creates the Project at connect time, so CI never creates. +- D6. The overwrite case (identity set to another project's) is addressed by scoped CI tokens on the platform, not by Composer. + +## A. Getting connected + +| # | Scenario | Expected | Covered by | +| --- | --- | --- | --- | +| A1 | New user connects a repo with no Composer config in Console, types the app name "store". | Console checks `store` is free in the workspace, reserves it, opens the setup PR with `logicalId = "store"` and `name: "Store"` in `module.mjs`. | D1, D5 | +| A2 | Setup PR's own workflow deploys a preview before merge. | The preview deploys into the reserved Project's branch. Nothing is created by CI. | D5 (open: today CI creates the Project) | +| A3 | User merges the setup PR. | Push to main deploys production into the same Project. Link becomes active. | D2 | +| A4 | User connects a repo that already has a Composer config with `logicalId = "store"`. | Console reads the identifier from the file, checks it is free, reserves it, creates the Project, and links. | D5; needs the Console inspector to read a hoisted constant | +| A5 | Two people connect two repos with the app name "store" in the same workspace. | The second connect is refused with "a project with this name exists." | Existing conflict check; TD-034 covers the concurrent race | +| A6 | A repo connected before this change has a module with a single string `module('store', …)`. | Keeps deploying unchanged until the author adopts the two-field form. The migration rule is open: does a single string mean `logicalId` and `name` both, with no behaviour change? | Open | +| A7 | A Project created before `logicalId` existed, with display name "Bright Peach Wallaby" and no identifier. | After D2 it no longer resolves by name. It needs a backfill from the link's reserved name, or the user sets the Project's `logicalId` in Console once. | Open: backfill vs Console edit | + +## B. Day-to-day edits that must be safe + +| # | Scenario | Expected | Covered by | +| --- | --- | --- | --- | +| B1 | Author changes the root `name` from "Store" to "Storefront". | Console label changes. Same Project, same stages, same data. | D1, D3 | +| B2 | Author changes a database's `name` from "Catalog" to "Catalogue". | Label changes. Same Database, same data. | D1, D3 | +| B3 | Someone renames the Project in Console. | On the next deploy the label returns to the code's `name`. If code has no `name`, the Console label stays. | D3 | +| B4 | Someone renames a Database in Console. | Same as B3. | D3 | +| B5 | Author moves a service into a nested module without changing its `logicalId`. | Its address changes (`api` → `billing.api`), so its identity changes. Plan output says so. Same as today under ADR-0051; not a regression, but should be named. | D1; plan output open | + +## C. Identity edits that must be deliberate and visible + +| # | Scenario | Expected | Covered by | +| --- | --- | --- | --- | +| C1 | Author changes the root `logicalId` from `store` to `storefront`, not understanding the consequence, and runs a local deploy. | Deploy stops: "No Project with logicalId `storefront`. You are creating a new project, not updating `store`. Pass `--create` to proceed." | D4 | +| C2 | Same edit, pushed to a connected repo. | CI deploy fails with the same message. The Project reserved for this repo is `store`; nothing is created. | D4, D5 | +| C3 | Author changes a child `logicalId` (`catalog` → `catalogue`). | Plan output names the consequence: delete `catalog`, create `catalogue`, data lost, use `name` for a label change. Whether this needs a flag is open. | D1; plan output open | +| C4 | Author sets the root `logicalId` to another repo's project (`billing`) by mistake, with a workspace-wide token (hand deploy or today's CI token). | Composer finds `billing` and deploys into it. Composer cannot tell this from C5. The edit is explicit in the diff, which is the improvement over today. The lock is D6. | D6 | +| C5 | Author sets the root `logicalId` to another project on purpose to take it over. | Same as C4. Works. | D2 | +| C6 | Same as C4, with a scoped CI token (future). | The platform refuses the write to `billing`. | D6, platform | + +## D. Lifecycle + +| # | Scenario | Expected | Covered by | +| --- | --- | --- | --- | +| D1 | User forks the repo to make a second, separate app. | They change `logicalId` to `store-eu` and run `prisma deploy --create`, or connect the fork in Console, which reserves the new identifier and creates the Project. | D4, D5 | +| D2 | User deletes the Project in Console, then pushes. | CI deploy fails: no Project with `logicalId` `store`, and CI does not create. The user reconnects in Console or runs `--create` by hand. | D4, D5 | +| D3 | User renames the repo on GitHub. | Nothing changes. The link is by numeric repo id. | Existing | +| D4 | User unlinks the repo in Console and deploys from a laptop with a service token. | Works: the Project is found by `logicalId`. | D2 | +| D5 | User destroys a stage, or production, through the control API. | Found by `logicalId`, find-only. Unchanged. | D2 | +| D6 | User imports an existing database into the module (control plane import spec). | They declare `logicalId: 'catalog'` and bind the row by platform id. Omitting `name` keeps the Console label. | D1, D3 | + +## E. Paths without a Console link + +| # | Scenario | Expected | Covered by | +| --- | --- | --- | --- | +| E1 | Hand-written repo, `PRISMA_SERVICE_TOKEN` in Actions secrets, never connected. | First deploy needs `--create` in the workflow, or the user creates the Project by hand first. Later deploys find it. | D4 | +| E2 | Developer deploys from a laptop with a personal service token into a connected repo's Project. | Found by `logicalId`. Same Project as CI. | D2 | +| E3 | Developer runs `prisma deploy --name demo-42` to deploy under a different app name (documented today). | Open: with two fields, `--name` either overrides the root `logicalId` (and needs `--create`) or is removed. | Open | + +## F. Operator-facing + +| # | Scenario | Expected | Covered by | +| --- | --- | --- | --- | +| F1 | Support looks at a Project and asks which repo deploys to it and under which identifier. | The link row shows repo id and `logicalId`; the Project row shows the same `logicalId`. | D5 | +| F2 | A deploy created a Project nobody expected (today's composer#103 class of incident). | Cannot happen from CI after D5. From a laptop, it required `--create`. | D4, D5 | + +## Open items this list surfaces + +1. D5: create the Project at connect time for the setup-PR path (A2, C2, D2). +2. A6: migration rule for existing single-string modules. +3. A7: backfill of `logicalId` on old Projects, or a one-time Console edit. +4. C3 and B5: plan output for identity changes below the root, and whether it needs consent. +5. E3: the fate of `--name`. diff --git a/.drive/projects/node-identity-and-consent/design-notes.md b/.drive/projects/node-identity-and-consent/design-notes.md new file mode 100644 index 000000000..8bc907f13 --- /dev/null +++ b/.drive/projects/node-identity-and-consent/design-notes.md @@ -0,0 +1,207 @@ +# Design notes — node-identity-and-consent + +This is the full design from the discussion on 2026-10-14, written for handoff. Phase 1 ("Delivery order" below) is delivered by the `deployment-identity` project; this project delivers phases 2 to 4. Companion material: [assets/user-scenarios.md](assets/user-scenarios.md) and [assets/logicalid-walkthrough.md](assets/logicalid-walkthrough.md). + +## The refined topic + +Søren's concern was that renaming a Composer root module can redirect a +deploy into another repository's Project. The discussion reframed it: the +defect is that one string on every node is at once the identity, the label +and the address, so an edit that looks cosmetic changes identity. The fix is +to separate identity from label on every node and make every identity change +and every data-losing deploy an explicit act. The security boundary Søren +also named, the workspace-wide CI token, is a platform concern and is left to +scoped tokens. + +## Decisions + +### D1. Every node has `logicalId` and `name` + +`logicalId` is the node's identity: required, hand-written, stable over time. +`name` is a label: optional. Only `logicalId` reaches the platform row's +`logicalId` and the topology. `name` reaches the display name and nothing +else. Absent `name` means Composer does not manage the label. + +`logicalId` lives on the node definition (`postgres({ logicalId })`, +`module({ logicalId, name }, …)`), not on the `provision()` call. The +provision-time `id` override stays for its one purpose: provisioning the +same node twice, or a reusable module from a package whose consumer needs a +different address. Platform rows already have ids; these are not those. + +*Why:* with two fields, a label edit is harmless and an identity edit is +visibly deliberate in the diff. Composer can tell the two apart, which it +cannot today, so it can also name the consequence. + +*Assumes:* authors accept writing both fields. Conveniences are added only +if that proves necessary. + +### D2. The Project is selected by `logicalId` only + +The display-name fallback in container resolution is removed. + +*Why:* the fallback is what let a name match adopt an unrelated Project. +ADR-0051 already calls it a compatibility path. + +*Assumes:* D10 backfills old Projects first. + +### D3. The graph is Composer's image of the system + +Values edited on the platform are set back on the next deploy. This +includes labels, when `name` is set. + +*Why:* one writer per field. Two writers fight on every deploy. + +*Consequence to record:* the control plane's import spec says an adopted +resource keeps its label. Under D3 that holds when the declaration omits +`name`. + +### D4. Creating a Project is explicit: `--create` + +A deploy whose root `logicalId` matches no Project refuses, says it would +create a new project rather than update one, and names `--create`. This is a +mode flag making a non-default action explicit, not a consent; nothing is +lost by creating. + +*Why:* a mistaken identity edit lands on "you are about to create a project" +instead of a silent empty Project and a production outage. + +### D5. Console creates the Project at connect time + +The setup-PR path adopts what the "connect without a setup PR" path already +does: create the Project with the reserved `logicalId` and the link together. +CI never creates a Project. + +*Why:* it sets the Project's `logicalId` and the module's in tandem, avoids +any change to the generated workflow or the deploy action, and has no +coupling to a Composer version that knows `--create`. Old and new Composer +both find the Project by `logicalId`. + +*Consequences to record:* a pending link carries a `projectId` from the +start, so "Remove setup" and the abandoned-PR archive path must delete the +empty Project deliberately. Console passes a region at create. + +### D6. Overwriting another repo's Project is a platform concern + +Setting the root `logicalId` to another project's identifier and deploying +is an explicit instruction. Composer cannot distinguish a mistake from a +takeover without reading build history, which would drag GitHub back in. +The lock is scoped CI tokens, which the control plane's ADR-010 defers. + +### D7. The single-string form stops loading + +`module('store', …)` and `postgres({ name })` fail with an error naming the +two-field form. No deprecation period: Composer is pre-1.0. + +### D8. Data-losing deploys use the ORM's consent model + +A deploy whose plan would delete a Database or Bucket, or re-identify a node, +asks one question per subject, answered by `--delete ` or +`--rename :`. `--yes` never consents. There is no `--force`. +Non-interactive runs fail with `CLI.CONSENT_REQUIRED` naming the flags. The +first implementation is laptop-only for destructive deploys: CI fails +closed and a human consents interactively. + +`--rename` is the survivable-rename mechanism the branch-topology spec left +open. It depends on the platform's update path accepting a new `logicalId` +on service, database and bucket rows, which the import project is adding. + +*Why:* consistency with the ORM, and the ORM's reasoning holds here: consent +describes a transition, and only the command performing it needs the +description. + +### D9. Console approval of CI consent is the target, deferred + +The build record would carry the questions, Console would show them, and a +click would answer. It needs scoped tokens to be safe. Rejected for now as +a large piece of platform work. + +### D10. Backfill `logicalId` on old Projects once + +A platform script sets `logicalId` from the link's reserved module name on +every linked Project that has none. A Project with neither is set by hand in +Console. + +### Smaller consequences + +- `--name` on `prisma deploy` goes away. Re-identifying the root is + `--rename store:storefront`. +- The generated `module.mjs` hoists the root `logicalId` into a named + constant with a one-line comment. The Console inspector must read that + constant, not only the `module("…")` literal. +- Composer stays unaware of GitHub. No `GITHUB_*` reads, no + source-repository lookups. + +## Alternatives rejected + +- **PR 355's approach**: Composer reads `GITHUB_REPOSITORY_ID` and checks + the source-repository listing. Makes Composer GitHub-aware, re-verifies at + deploy time what the platform decided at connect time, and is only a + safeguard by its own admission. +- **The OIDC exchange returns the app name and the action passes `--name`.** + Locks CI but makes the identity invisible in the repo, so a developer + cannot tell which Project `module.mjs` deploys to, and a local and a CI + deploy of the same file can target different Projects. +- **A platform-generated root `logicalId`** (opaque id, or readable slug with + a suffix). An opaque id is a foreign key in user code and fails on fork or + delete, which the control plane's ADR-011 already rejected. A suffixed slug + invites "tidying," which silently creates a new Project. +- **`logicalId` defaulting from `name`.** Reintroduces the footgun one level + down. +- **CI passes `--create`.** Makes the flag meaningless in CI and couples the + generated workflow to a Composer version. +- **Consent in code** (`renamedFrom`, `retired(...)` declarations, Terraform's + `moved` blocks). Prototyped and rejected in the ORM: the code would carry an + obsolete representation of the system after the transition, when only the + command performing it ever needed it. The hint goes to the command instead. +- **A committed consent file bound to a plan hash.** The hash differs per + stage, so a production consent blocks every preview deploy on the branch. + +## Open items and accepted trade-offs + +- A human with a workspace token can still deploy into any Project. Accepted + until scoped tokens exist. +- Destructive deploys cannot run from push-to-deploy. Accepted; D9 is the + path out. +- Plan output for identity changes below the root is part of D8, not a + separate slice. +- TD-034 in the control plane (two concurrent connects reserving the same + module name) is unchanged by this design. + +## Persona cross-pollination + +The architect pass settled shape: two fields, identity on the definition, +`logicalId` kept as the shared vocabulary with the platform. The principal +engineer concerns surfaced during it and were settled by adopting an +existing mechanism each time: the ORM's consent model for data loss, the +Console's existing create-and-link path for D5, and the resolver's existing +find-only mode for `--create`. + +## Where the work lands + +| Change | Repo | +| --- | --- | +| `logicalId` and `name` on every node; single-string form refused; `--create`; ORM consent model; `--name` removed; ADR amending ADR-0006, ADR-0024, ADR-0051 | prisma/composer | +| Console creates the Project at connect for the setup-PR path; empty-Project cleanup on release; inspector reads the hoisted constant; generated `module.mjs` uses the two-field form; backfill script | prisma/pdp-control-plane | +| Nothing required. Close PR 355. | prisma/cloud-deploy-action | +| Scoped CI tokens; Console approval of consent | platform, later | + +## Delivery order + +Agreed 2026-10-14. Each phase ships on its own. + +1. **Close the immediate problem.** Composer finds the project only by its + identifier (the display-name fallback goes). A one-time platform script + gives old projects an identifier from the name the repo was connected + under. Composer refuses to create a project unless `--create` is passed. + Console creates the project when the repo is connected, so CI never + creates one. No change to how modules are written. PR 355 closes. +2. **Separate identity from label.** Every node gets a required `logicalId` + and an optional `name`. The single-string form stops loading. `--name` + on deploy goes away. The generated setup files and the Console's reader + of the module file follow. One release, because it breaks the API. +3. **Ask before losing data.** The ORM's consent model: `--delete` and + `--rename` per subject, `--yes` never consents, CI fails closed and a + human consents from a laptop. Waits on the platform allowing a row's + identifier to change, which the import project adds. +4. **Platform.** CI tokens scoped to one project, then Console approval of + consent from CI. diff --git a/.drive/projects/node-identity-and-consent/plan.md b/.drive/projects/node-identity-and-consent/plan.md new file mode 100644 index 000000000..50b680631 --- /dev/null +++ b/.drive/projects/node-identity-and-consent/plan.md @@ -0,0 +1,42 @@ +# Project Plan — node-identity-and-consent + +## Summary + +Handoff plan. The slices below are the phases from the design, in the order they must ship. The receiving team decomposes each into slices of its own at pickup. Linear project: [Node identity and consent](https://linear.app/prisma-company/project/node-identity-and-consent-9ec5f9f522c0) (P-TML-1157). Builds on `deployment-identity` (P-TML-1156), which must be in production first. + +**Spec:** `.drive/projects/node-identity-and-consent/spec.md` + +## Phases + +### Phase 2 — Separate identity from label (Composer, then control plane) + +Every node definition takes `logicalId` (required) and `name` (optional); Load uses `logicalId` for the address; the single-string form fails to load with an error naming the fix; `--name` is removed from `prisma deploy`; guides, examples and the core-concepts skill move to the two-field form; a new decision record states the model and amends the records on node naming, stage resolution and node addresses. Then the control plane: the setup-PR generator writes the two-field root with a hoisted `logicalId` constant and a one-line comment, and the module-file inspector reads that constant. One Composer release. + +- **Builds on:** `deployment-identity` in production. +- **Hands to:** phase 3 — Composer can tell a label edit from an identity edit. + +### Phase 3 — Ask before losing data (Composer, with a platform dependency) + +The ORM's consent model in `prisma deploy`: one question per subject whose row would be deleted or re-identified, answered by `--delete ` or `--rename :`; `--yes` never consents; non-interactive runs fail with `CLI.CONSENT_REQUIRED`. `--delete` can ship first. `--rename` waits on the platform's update path accepting a new `logicalId` on service, database and bucket rows (the import project adds it). + +- **Builds on:** phase 2. +- **Hands to:** phase 4 — CI deploys that need consent fail closed with a named question the platform could carry. + +### Phase 4 — Platform (control plane) + +4a. CI tokens scoped to the Project the repo is connected to (the narrowing the control plane's CI-trust decision record deferred). 4b. Console approval: the build record carries the consent questions, Console shows them, an operator answers, the CI run re-runs with the answers. + +- **Builds on:** 4a on nothing beyond `deployment-identity`; 4b on 4a and phase 3. +- **Hands to:** close-out. + +## Sequencing + +- Stack: 2 → 3 → 4b. +- Parallel: 4a alongside phases 2 and 3. + +## Close-out (required) + +- [ ] Verify all acceptance criteria in `.drive/projects/node-identity-and-consent/spec.md` +- [ ] Migrate long-lived docs into `docs/` (the ADR; the design notes' decisions and rejected alternatives belong in it) +- [ ] Strip repo-wide references to `.drive/projects/node-identity-and-consent/**` +- [ ] Delete `.drive/projects/node-identity-and-consent/` diff --git a/.drive/projects/node-identity-and-consent/spec.md b/.drive/projects/node-identity-and-consent/spec.md new file mode 100644 index 000000000..acf0e1ad5 --- /dev/null +++ b/.drive/projects/node-identity-and-consent/spec.md @@ -0,0 +1,72 @@ +# Purpose + +Make every identity change in a Composer graph deliberate and every data-losing deploy consented. Today one string on each node is at once its identity, its label and its address, so an edit that looks cosmetic re-identifies a node and the next deploy deletes a database or forks a Project. After this project, a node's identity and its label are two fields, a label edit changes nothing structural, an identity edit is visible in the diff and asks before it destroys anything, and the platform's CI credential can only reach the Project it was issued for. + +This project builds on `deployment-identity`, which already makes Composer find a Project by identifier only and create one only when told. That project closes the reported problem; this one makes the model right. It is written for handoff to another team. + +# At a glance + +Three phases, each shipping on its own. + +**Phase 2: separate identity from label.** +Every node definition carries a required `logicalId` and an optional `name`. `logicalId` is hand-written, stable over time, and the only value that reaches the platform row's `logicalId` and the application topology. `name` reaches the display name and nothing else; absent, Composer does not manage the label. The provision-time `id` override stays for provisioning the same node twice or a packaged module whose consumer needs a different address. The single-string form, `module('store', …)` and `postgres({ name })`, stops loading with an error naming the two-field form. `--name` on `prisma deploy` goes away. The generated setup files and the Console's reader of the module file follow. + +**Phase 3: ask before losing data.** +A deploy whose plan would delete a Database or Bucket, or re-identify a node, uses the ORM's consent model: one question per subject, answered by `--delete ` or `--rename :`; `--yes` never consents; there is no `--force`; non-interactive runs fail with `CLI.CONSENT_REQUIRED` naming the flags. CI fails closed and a human consents from a laptop. `--rename` updates the platform row's `logicalId` in place, which is the survivable rename the branch-topology design left open. + +**Phase 4: platform.** +CI tokens scoped to the Project the repo is connected to, then Console approval of consent questions raised by a CI deploy. + +# Non-goals + +- Conveniences that derive `logicalId` from `name`. Required everywhere first; conveniences only if that proves necessary, and never a default from `name`. +- Consent recorded in code (`renamedFrom`, `retired(...)`, Terraform-style `moved` blocks). Rejected; see design notes. +- A committed consent file bound to a plan hash. Rejected; see design notes. +- A deprecation period for the single-string form. Composer is pre-1.0. +- Changing the deploy action or the generated workflow for anything in phases 2 and 3. + +# Place in the larger world + +- Composer: the node constructors in `@prisma/composer` and the Prisma Cloud extension; Load, which assigns each node its address; the `prisma deploy` command; and container resolution, which finds the Project before Alchemy runs. +- Control plane: the setup-PR generator (`planComposerSetup.ts`, `buildModule`), the module-file inspector that reads the declared name at connect, and the resource update path that the import project (`projects/composer-import/spec.md`) adds for binding rows by platform id. Phase 3's `--rename` depends on that path accepting a new `logicalId` on service, database and bucket rows. +- The control plane's decision record on CI trust ("a connected repository is the trust boundary for CI deploys") mints a workspace-wide token and defers narrowing it. Phase 4 is where that narrowing lands. The control plane's CI-builds spec requires that every screen a user acts on is in Console, which is why consent approval belongs there rather than in GitHub. +- The ORM's CLI Style Guide, "Destructive operation confirmation", is the consent model to copy exactly. + +# Cross-cutting requirements + +- `logicalId` is the only identity. No code path may match a platform row by display name. +- Every node kind that lowers to a platform row passes its `logicalId`. A new node kind is not done until it does. Composer's decision record "a node's address is its logical ID on the platform" already requires this; it is restated here because phase 2 touches every constructor. +- Consent is per subject and never defaulted. `--yes` and `-y` accept prompt defaults only. +- The graph is Composer's image of the system: values edited on the platform are set back on the next deploy. The one deliberate exception is a label whose declaration omits `name`. +- Composer stays unaware of GitHub. + +# Transitional-shape constraints + +- Phase 2 lands in one Composer release, because it breaks every existing module. The generated setup files and the Console inspector ship before or with it. +- Phase 3's `--delete` can ship before `--rename`; `--rename` waits on the platform update path. Until then a re-identified node is delete-and-create with consent. +- Phase 4's two halves are independent: scoped tokens first, approval second. + +# Project DoD + +- [ ] A module written with the two-field form deploys; the same module with a single-string node fails to load with an error naming the fix. +- [ ] Renaming a database's `name` and deploying changes its Console label and nothing else. Renaming its `logicalId` without a statement fails with `CLI.CONSENT_REQUIRED`; with `--rename old:new` the same row keeps its data and carries the new identifier; with `--delete old` the row is deleted and a new one created. Each proven against a real Project. +- [ ] A push-to-deploy run whose plan would delete a database fails closed and names the flags; no row changes. +- [ ] A fresh Console connect writes the two-field root with a hoisted `logicalId` constant, and connecting a repo that brings its own two-field config reads the identifier correctly. +- [ ] A CI token minted for a connected repo cannot read or write another Project in the workspace. +- [ ] A new Composer decision record states the two-field model and the consent model, and amends the records on node naming, stage resolution and node addresses. The guides, examples and the core-concepts skill use the two-field form throughout. + +# Open questions + +- Whether Console approval (phase 4b) is built in this project or spun out once scoped tokens exist. Decide at phase 4 planning. +- Whether the plan output for an identity change below the root needs its own flag beyond the consent question. Decide in phase 3 slice planning. + +# References + +- Design notes with every decision, its reasoning, and the rejected alternatives: [design-notes.md](design-notes.md). +- User scenarios the design must cover: [assets/user-scenarios.md](assets/user-scenarios.md). +- Worked example of the two-field model: [assets/logicalid-walkthrough.md](assets/logicalid-walkthrough.md). +- Predecessor project: `.drive/projects/deployment-identity/`. +- ORM consent model: prisma/orm `docs/CLI Style Guide.md`, section "Destructive operation confirmation". The two ORM changes that introduced it are "migration plan asks before a lossy type change" (commit 07a7bc06) and "migration plan requires consent for destructive auto-baselines" (commit c5a8b5ad). +- Composer decision records, in `docs/design/90-decisions/`: ADR-0006 "every node is named", ADR-0024 "a stage is a deploy-time environment resolved to Project and Branch", ADR-0050 "Composer runs as prisma deploy and prisma dev", ADR-0051 "a node's address is its logical ID on the platform". +- Control plane, prisma/pdp-control-plane: ADR-010 "a connected repository is the trust boundary for CI deploys", ADR-011 "project slug as the deploy-target identity", `projects/branch-topology/spec.md`, `projects/composer-import/spec.md`, and `docs/compute/GitHubImport.md`. +- Linear: Node identity and consent (Terminal team). From 878f1b50261099b22adfdde5da5fcff3c53938e4 Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 3 Nov 2026 09:33:01 +0100 Subject: [PATCH 2/2] docs(drive): bring both designs up to date with full-path addresses and environments Cite the decision record that makes a node's address its full path from the app name, which superseded the one the designs cited and is the record that keeps the display-name fallback. Use full-path addresses and the word environment in examples. Record how the scenarios' open items were settled, and put logicalId on the node definition in the worked example. Add the open question this raises: on a child, the value an author writes is one segment of the logicalId. Signed-off-by: willbot Signed-off-by: Will Madden --- .../deployment-identity/design-notes.md | 2 +- .drive/projects/deployment-identity/plan.md | 2 +- .drive/projects/deployment-identity/spec.md | 6 +- .../assets/logicalid-walkthrough.md | 112 +++++++++++------- .../assets/user-scenarios.md | 43 ++++--- .../node-identity-and-consent/design-notes.md | 11 +- .../node-identity-and-consent/plan.md | 2 +- .../node-identity-and-consent/spec.md | 6 +- 8 files changed, 112 insertions(+), 72 deletions(-) diff --git a/.drive/projects/deployment-identity/design-notes.md b/.drive/projects/deployment-identity/design-notes.md index 72039d479..245473f99 100644 --- a/.drive/projects/deployment-identity/design-notes.md +++ b/.drive/projects/deployment-identity/design-notes.md @@ -8,7 +8,7 @@ ## The model -The Project's `logicalId` is the app name (ADR-0051). Container resolution matches on it and on nothing else. If no Project matches, the deploy stops unless `--create` was passed. The platform creates the Project when a repo is connected, so a connected repo's CI run always finds its Project and never needs `--create`. +The Project's `logicalId` is the app name: the decision record "a node's address is its full path from the app name" makes the root's address the app name, and every address is its node's `logicalId`. Container resolution matches on it and on nothing else. If no Project matches, the deploy stops unless `--create` was passed. The platform creates the Project when a repo is connected, so a connected repo's CI run always finds its Project and never needs `--create`. ``` prisma deploy module.ts # finds Project by logicalId, or refuses diff --git a/.drive/projects/deployment-identity/plan.md b/.drive/projects/deployment-identity/plan.md index 62caff56c..850aa7222 100644 --- a/.drive/projects/deployment-identity/plan.md +++ b/.drive/projects/deployment-identity/plan.md @@ -26,7 +26,7 @@ A script under the control plane's scripts convention: for every live `ProjectSc ### Slice 3 — Composer finds by `logicalId` only and creates only with `--create` (TML-3581, Composer) -`resolveProject` drops the display-name match. `ResolveContainerOptions.ensure` becomes a mode with three values: a plain deploy finds the Project or refuses, `deploy --create` finds it or creates it, and destroy finds it or reports that nothing is deployed. The mode is threaded from a new `create` option on the `deploy` operation and `prisma deploy --create` through `LocateContainerInput` and the extension's `ensureContainer`. A miss without `--create` is a structured `DEPLOY.PROJECT_NOT_FOUND` error with the message from the design notes. The decision records on stage resolution and node addresses amended, deploying guide and core-concepts skill updated, PR 355 closed. +`resolveProject` drops the display-name match. `ResolveContainerOptions.ensure` becomes a mode with three values: a plain deploy finds the Project or refuses, `deploy --create` finds it or creates it, and destroy finds it or reports that nothing is deployed. The mode is threaded from a new `create` option on the `deploy` operation and `prisma deploy --create` through `LocateContainerInput` and the extension's `ensureContainer`. A miss without `--create` is a structured `DEPLOY.PROJECT_NOT_FOUND` error with the message from the design notes. The decision records on environment resolution and on full-path node addresses are amended, deploying guide and core-concepts skill updated, PR 355 closed. - **Builds on:** slices 1 and 2 in production. - **Hands to:** close-out. diff --git a/.drive/projects/deployment-identity/spec.md b/.drive/projects/deployment-identity/spec.md index 893375a67..d87d8123c 100644 --- a/.drive/projects/deployment-identity/spec.md +++ b/.drive/projects/deployment-identity/spec.md @@ -23,7 +23,7 @@ Four changes, two in Composer and two on the platform: # Place in the larger world -- Composer: container resolution, which finds or creates the Project and Branch before Alchemy runs (decision record "a stage is a deploy-time environment resolved to Project and Branch", amended by "a node's address is its logical ID on the platform"), and the `prisma deploy` command. +- Composer: container resolution, which finds or creates the Project and Branch before Alchemy runs (decision record "a stage is a deploy-time environment resolved to Project and Branch"), and the `prisma deploy` command. The decision record "a node's address is its full path from the app name" makes the root's address, the app name, the Project's `logicalId`, and it is the record that still describes the display-name fallback as a compatibility path. - Control plane: `connectRepositoryForComposer` in `services/console-compute/lib/composerConnect.server.ts`, `connectWorkspaceToScmRepo`, `createProject` and its `reflectProjectOnLink` step, which becomes unnecessary once the Project exists before the link. The GitHub Actions token exchange and `cloud-deploy-action` are untouched. - The control plane's decision record "project slug as the deploy-target identity" made the module name the Project's `logicalId`. This project finishes that work by removing the display-name fallback it kept for older Projects. - prisma/composer#355 is closed in favour of this project. The decision record it proposed, "GitHub project selection checks active repository connections", is not adopted. @@ -47,7 +47,7 @@ Four changes, two in Composer and two on the platform: - [ ] Releasing a setup ("Remove setup") or closing the setup PR unmerged deletes the Project the connect created, and only that one. - [ ] Every linked Project in production has a `logicalId`; the backfill reports zero remaining. - [ ] `prisma deploy --create` creates a Project from a laptop; without the flag the same deploy refuses. -- [ ] The two Composer decision records named above are amended. The deploying guide and the core-concepts skill say that a deploy finds its Project by `logicalId` and needs `--create` to make one. The guide's `prisma deploy module.ts --name demo-42` example still works, with `--create` on its first run. +- [ ] The Composer decision records "a stage is a deploy-time environment resolved to Project and Branch" and "a node's address is its full path from the app name" are amended: the fallback is gone, and creating a Project needs `--create`. The deploying guide and the core-concepts skill say that a deploy finds its Project by `logicalId` and needs `--create` to make one. The guide's `prisma deploy module.ts --name demo-42` example still works, with `--create` on its first run. - [ ] prisma/composer#355 closed with a comment pointing at this project. # Open questions @@ -58,6 +58,6 @@ Four changes, two in Composer and two on the platform: - Design synthesis and scenarios: `.drive/projects/node-identity-and-consent/design-notes.md` and `assets/`. - Slack thread with Søren, 2026-10-14, and prisma/composer#355. -- Composer decision records, in `docs/design/90-decisions/`: ADR-0006 "every node is named", ADR-0024 "a stage is a deploy-time environment resolved to Project and Branch", ADR-0050 "Composer runs as prisma deploy and prisma dev", ADR-0051 "a node's address is its logical ID on the platform". +- Composer decision records, in `docs/design/90-decisions/`: ADR-0006 "every node is named", ADR-0024 "a stage is a deploy-time environment resolved to Project and Branch", ADR-0050 "Composer runs as prisma deploy and prisma dev", ADR-0057 "a node's address is its full path from the app name" (it superseded ADR-0051), and ADR-0059 "a place the app runs is called an environment, not a stage or a branch". - Control plane, prisma/pdp-control-plane: ADR-010 "a connected repository is the trust boundary for CI deploys" (it mints a workspace-wide token), ADR-011 "project slug as the deploy-target identity", `docs/CiOidcTokenExchange.md`, and `docs/compute/GitHubImport.md` for the repository link lifecycle. - Linear: Deployment identity (Terminal team). diff --git a/.drive/projects/node-identity-and-consent/assets/logicalid-walkthrough.md b/.drive/projects/node-identity-and-consent/assets/logicalid-walkthrough.md index 96ffcfdfc..d69a459e0 100644 --- a/.drive/projects/node-identity-and-consent/assets/logicalid-walkthrough.md +++ b/.drive/projects/node-identity-and-consent/assets/logicalid-walkthrough.md @@ -1,6 +1,7 @@ # Walkthrough: `logicalId` and `name` on every node -A working note for the design discussion. Not a spec. +A working note from the design discussion. Not a spec. Where it differs from +[../design-notes.md](../design-notes.md), the design notes win. ## 1. The system we use throughout @@ -8,15 +9,19 @@ A small shop. One root module, a Postgres database, a web service, an API service, and a reusable auth module that owns its own database and service. ``` -store root module → Project -├── catalog postgres → Database -├── web compute → service -├── api compute → service -└── auth module (no row) - ├── database postgres → Database address auth.database - └── service compute → service address auth.service +node kind platform row address (= the row's logicalId) +store root module Project store +├── catalog postgres Database store.catalog +├── web compute service store.web +├── api compute service store.api +└── auth module (no row) store.auth + ├── database postgres Database store.auth.database + └── service compute service store.auth.service ``` +Addresses are full paths from the app name, as the decision record "a node's +address is its full path from the app name" requires. + ## 2. Today ```ts @@ -34,9 +39,9 @@ nodes use. The three footguns, all caused by editing that one string: | Edit | What the author meant | What happens | | --- | --- | --- | -| `catalog` → `catalogue` | fix spelling | next deploy deletes the `catalog` Database and creates an empty `catalogue` | +| `catalog` → `catalogue` | fix spelling | next deploy deletes the `store.catalog` Database and creates an empty `store.catalogue` | | `store` → `storefront` | nicer name | next deploy creates a second Project; `store` is left behind with its data | -| `store` → `billing` (another repo's project in the workspace) | copy-paste, or a "rename" | next deploy reconciles into the `billing` Project and overwrites its production stage | +| `store` → `billing` (another repo's project in the workspace) | copy-paste, or a "rename" | next deploy reconciles into the `billing` Project and overwrites its production environment | The third row is Søren's case. The first two are the same defect. @@ -47,18 +52,38 @@ Only `logicalId` reaches the platform row's `logicalId` and the topology. `name` reaches the row's display name and nothing else. Omitting `name` means Composer does not manage the label. +The fields go on the node's definition, not on the `provision()` call. The +`id` override on `provision()` stays for provisioning the same node twice, or +for a packaged module whose consumer needs a different address. + ```ts +// web.ts +export default service({ logicalId: 'web', name: 'Web', … }); +// api.ts +export default service({ logicalId: 'api', … }); +// auth module, from a package +export default module({ logicalId: 'auth', … }, …); + +// module.ts export default module({ logicalId: 'store', name: 'Store' }, ({ provision }) => { const catalog = provision(postgres({ logicalId: 'catalog', name: 'Catalog', contract, config })); - const auth = provision(authModule, { logicalId: 'auth', secrets: { signingKey: envSecret('AUTH_SIGNING_SECRET') } }); - provision(web, { logicalId: 'web', name: 'Web', deps: { db: catalog, auth: auth.rpc } }); - provision(api, { logicalId: 'api', deps: { db: catalog } }); + const auth = provision(authModule, { secrets: { signingKey: envSecret('AUTH_SIGNING_SECRET') } }); + provision(web, { deps: { db: catalog, auth: auth.rpc } }); + provision(api, { deps: { db: catalog } }); }); ``` -The address is still the path of `logicalId`s: `auth.database`. Nothing -else changes in the model. Local dev, state keys, Alchemy naming, and the -topology payload all keyed off the address already. +The address is still built the same way: the root's value, then each +ancestor's, joined by dots (`store.auth.database`). Local dev, deploy state, +Alchemy resource IDs and the topology all key off the address already. + +**Unsettled: what the field on a child holds.** The decision record "a node's +address is its full path from the app name" makes the full address the +platform `logicalId`. So for a child, the field written on the definition +(`'catalog'`) is one segment of the `logicalId`, not the `logicalId` itself +(`store.catalog`). Only on the root are the two the same. Whether the child's +field should still be called `logicalId`, or something that names the +segment, is open; see the spec's open questions. Same three edits, re-run: @@ -127,15 +152,16 @@ URL, which is the Søren case by a different road. They want this repo to take over the `billing` Project. They set `logicalId: 'billing'`. The deploy finds that Project and reconciles into it. -The experience should make the takeover visible before it is irreversible: +Composer asks nothing for the switch itself: the author typed the other +project's identity on purpose, and Composer cannot tell a deliberate takeover +from a mistake without reading the platform's build history. Stopping the +mistake is the scoped CI token's job (D6). -``` -Project "billing" (proj_…) exists and holds a production deploy from -another source. Continue? (--yes) -``` - -Whether Composer asks, or just logs, is a separate decision. The point is -that the author typed the other project's identity on purpose. +The consent model still applies to what the reconcile would do inside +`billing`. Any database or bucket there that this repo's graph does not +declare would be deleted, so the deploy asks one question per subject, such as +`--delete billing.ledger`, and fails in CI until a human answers from a +laptop. ### 5c. "Tidying" a generated value @@ -157,17 +183,21 @@ names) and users accept it. ### 5e. A child rename -`logicalId: 'catalog'` → `'catalogue'` on the database. Composer plans to -delete `catalog` and create `catalogue`. Today that is silent. With the -field explicit, the plan output should name the consequence: +`logicalId: 'catalog'` → `'catalogue'` on the database, so its address goes +from `store.catalog` to `store.catalogue`. Today the next deploy deletes the +old database and creates an empty one, silently. With the field explicit, the +deploy asks first, the way the ORM's migration commands do (D8): ``` -~ catalog (Database): logicalId changed → delete "catalog", create "catalogue". - Data in "catalog" will be lost. Use `name` to change the label instead. +store.catalog (Database) is no longer declared, and store.catalogue is new. +Its data would be lost. To change only the label, edit `name` instead. +→ Pass --rename store.catalog:store.catalogue to keep the database +→ Pass --delete store.catalog to delete it ``` -That message is only possible because the two fields exist: Composer can -tell a label edit from an identity edit. +Non-interactively, as in CI, the deploy fails with `CLI.CONSENT_REQUIRED` and +names both flags. The advice to edit `name` is only possible because the two +fields exist: Composer can tell a label edit from an identity edit. ## 6. Where the value comes from in each flow @@ -190,14 +220,12 @@ local deploy and a CI deploy of the same file can target different Projects. That contradicts "the graph is Composer's image." Not recommended as the primary path; the token scope is the platform's job (ADR-010). -## 7. Open points this note surfaces - -1. Which shape for the root value: 4a is recommended. 4b and 4c are - rejected above. -2. Whether a root `logicalId` change that lands on an existing Project with - deploys from another source should warn, confirm, or just proceed (5b). -3. The plan output for identity changes on children (5e) is a separate, - valuable slice. -4. `--name` on the CLI today overrides the root's single string. With two - fields it should become a `logicalId` override, or be removed. The - generated workflow does not use it. +## 7. How the points this note raised were settled + +1. The root value is a hand-written slug that Console validates and reserves + at connect (4a). It is never generated by the platform. +2. A root `logicalId` that lands on an existing Project proceeds; consent + covers the data the reconcile would delete there (5b). +3. Identity changes on children are consent questions, answered by + `--rename` or `--delete` (5e). +4. `--name` is removed. diff --git a/.drive/projects/node-identity-and-consent/assets/user-scenarios.md b/.drive/projects/node-identity-and-consent/assets/user-scenarios.md index 0f5ae8443..a64133204 100644 --- a/.drive/projects/node-identity-and-consent/assets/user-scenarios.md +++ b/.drive/projects/node-identity-and-consent/assets/user-scenarios.md @@ -2,38 +2,45 @@ Companion to [logicalid-walkthrough.md](logicalid-walkthrough.md). Each row is a concrete situation, what the user expects, and which decision covers -it. "Open" marks the ones the discussion has not settled. +it. The decisions are listed in full, with their reasoning, in +[../design-notes.md](../design-notes.md). Addresses here are full paths from +the app name, as the decision record "a node's address is its full path from +the app name" requires. -Decisions so far, for reference: +Decisions, for reference: - D1. Every node has `logicalId` (identity, required, hand-written) and `name` (label, optional). - D2. The Project is selected by `logicalId` only. The display-name fallback is removed. - D3. Composer's graph is the source of truth; labels edited on the platform are set back on the next deploy. Omitting `name` leaves the label alone. - D4. Creating a Project is explicit: a deploy that would create one needs `--create`. -- D5. Proposed, open: Console creates the Project at connect time, so CI never creates. +- D5. Console creates the Project at connect time, so CI never creates one. - D6. The overwrite case (identity set to another project's) is addressed by scoped CI tokens on the platform, not by Composer. +- D7. The single-string form stops loading. No deprecation period. +- D8. Data-losing deploys use the ORM's consent model: `--delete ` or `--rename :`, `--yes` never consents, CI fails closed. +- D9. Console approval of consent raised by a CI deploy is the target, deferred. +- D10. A one-time platform script backfills `logicalId` on old linked Projects. ## A. Getting connected | # | Scenario | Expected | Covered by | | --- | --- | --- | --- | | A1 | New user connects a repo with no Composer config in Console, types the app name "store". | Console checks `store` is free in the workspace, reserves it, opens the setup PR with `logicalId = "store"` and `name: "Store"` in `module.mjs`. | D1, D5 | -| A2 | Setup PR's own workflow deploys a preview before merge. | The preview deploys into the reserved Project's branch. Nothing is created by CI. | D5 (open: today CI creates the Project) | +| A2 | Setup PR's own workflow deploys a preview before merge. | The preview deploys into the reserved Project's branch. Nothing is created by CI. | D5 | | A3 | User merges the setup PR. | Push to main deploys production into the same Project. Link becomes active. | D2 | | A4 | User connects a repo that already has a Composer config with `logicalId = "store"`. | Console reads the identifier from the file, checks it is free, reserves it, creates the Project, and links. | D5; needs the Console inspector to read a hoisted constant | | A5 | Two people connect two repos with the app name "store" in the same workspace. | The second connect is refused with "a project with this name exists." | Existing conflict check; TD-034 covers the concurrent race | -| A6 | A repo connected before this change has a module with a single string `module('store', …)`. | Keeps deploying unchanged until the author adopts the two-field form. The migration rule is open: does a single string mean `logicalId` and `name` both, with no behaviour change? | Open | -| A7 | A Project created before `logicalId` existed, with display name "Bright Peach Wallaby" and no identifier. | After D2 it no longer resolves by name. It needs a backfill from the link's reserved name, or the user sets the Project's `logicalId` in Console once. | Open: backfill vs Console edit | +| A6 | A repo connected before this change has a module with a single string `module('store', …)`. | The module fails to load with an error naming the two-field form. The author adds the fields and deploys again. | D7 | +| A7 | A Project created before `logicalId` existed, with display name "Bright Peach Wallaby" and no identifier. | After D2 it no longer resolves by name. The backfill sets its `logicalId` from the link's reserved name before the fallback is removed. A Project with no link has no automatic fix. | D10 | ## B. Day-to-day edits that must be safe | # | Scenario | Expected | Covered by | | --- | --- | --- | --- | -| B1 | Author changes the root `name` from "Store" to "Storefront". | Console label changes. Same Project, same stages, same data. | D1, D3 | +| B1 | Author changes the root `name` from "Store" to "Storefront". | Console label changes. Same Project, same environments, same data. | D1, D3 | | B2 | Author changes a database's `name` from "Catalog" to "Catalogue". | Label changes. Same Database, same data. | D1, D3 | | B3 | Someone renames the Project in Console. | On the next deploy the label returns to the code's `name`. If code has no `name`, the Console label stays. | D3 | | B4 | Someone renames a Database in Console. | Same as B3. | D3 | -| B5 | Author moves a service into a nested module without changing its `logicalId`. | Its address changes (`api` → `billing.api`), so its identity changes. Plan output says so. Same as today under ADR-0051; not a regression, but should be named. | D1; plan output open | +| B5 | Author moves a service into a nested module without changing its `logicalId`. | Its address changes (`store.api` → `store.billing.api`), so its identity changes, as it does today. The deploy would delete its rows, so it asks for consent: `--rename store.api:store.billing.api` keeps them, `--delete store.api` replaces them. | D1, D8 | ## C. Identity edits that must be deliberate and visible @@ -41,7 +48,7 @@ Decisions so far, for reference: | --- | --- | --- | --- | | C1 | Author changes the root `logicalId` from `store` to `storefront`, not understanding the consequence, and runs a local deploy. | Deploy stops: "No Project with logicalId `storefront`. You are creating a new project, not updating `store`. Pass `--create` to proceed." | D4 | | C2 | Same edit, pushed to a connected repo. | CI deploy fails with the same message. The Project reserved for this repo is `store`; nothing is created. | D4, D5 | -| C3 | Author changes a child `logicalId` (`catalog` → `catalogue`). | Plan output names the consequence: delete `catalog`, create `catalogue`, data lost, use `name` for a label change. Whether this needs a flag is open. | D1; plan output open | +| C3 | Author changes a database's identity, so its address goes from `store.catalog` to `store.catalogue`. | The deploy asks: delete `store.catalog` and lose its data, or rename it to `store.catalogue` and keep it. Answered by `--delete store.catalog` or `--rename store.catalog:store.catalogue`. The question says to use `name` for a label change. In CI the deploy fails and names both flags. | D1, D8 | | C4 | Author sets the root `logicalId` to another repo's project (`billing`) by mistake, with a workspace-wide token (hand deploy or today's CI token). | Composer finds `billing` and deploys into it. Composer cannot tell this from C5. The edit is explicit in the diff, which is the improvement over today. The lock is D6. | D6 | | C5 | Author sets the root `logicalId` to another project on purpose to take it over. | Same as C4. Works. | D2 | | C6 | Same as C4, with a scoped CI token (future). | The platform refuses the write to `billing`. | D6, platform | @@ -54,7 +61,7 @@ Decisions so far, for reference: | D2 | User deletes the Project in Console, then pushes. | CI deploy fails: no Project with `logicalId` `store`, and CI does not create. The user reconnects in Console or runs `--create` by hand. | D4, D5 | | D3 | User renames the repo on GitHub. | Nothing changes. The link is by numeric repo id. | Existing | | D4 | User unlinks the repo in Console and deploys from a laptop with a service token. | Works: the Project is found by `logicalId`. | D2 | -| D5 | User destroys a stage, or production, through the control API. | Found by `logicalId`, find-only. Unchanged. | D2 | +| D5 | User destroys an environment, such as staging or production, through the control API. | Found by `logicalId`, find-only. Unchanged. | D2 | | D6 | User imports an existing database into the module (control plane import spec). | They declare `logicalId: 'catalog'` and bind the row by platform id. Omitting `name` keeps the Console label. | D1, D3 | ## E. Paths without a Console link @@ -63,7 +70,7 @@ Decisions so far, for reference: | --- | --- | --- | --- | | E1 | Hand-written repo, `PRISMA_SERVICE_TOKEN` in Actions secrets, never connected. | First deploy needs `--create` in the workflow, or the user creates the Project by hand first. Later deploys find it. | D4 | | E2 | Developer deploys from a laptop with a personal service token into a connected repo's Project. | Found by `logicalId`. Same Project as CI. | D2 | -| E3 | Developer runs `prisma deploy --name demo-42` to deploy under a different app name (documented today). | Open: with two fields, `--name` either overrides the root `logicalId` (and needs `--create`) or is removed. | Open | +| E3 | Developer runs `prisma deploy --name demo-42` to deploy under a different app name (documented today). | `--name` is removed. Deploying the same code as a second app means changing the root's identity in the code and deploying with `--create`. Re-identifying an existing app is `--rename store:storefront`. | D4, D8 | ## F. Operator-facing @@ -72,10 +79,12 @@ Decisions so far, for reference: | F1 | Support looks at a Project and asks which repo deploys to it and under which identifier. | The link row shows repo id and `logicalId`; the Project row shows the same `logicalId`. | D5 | | F2 | A deploy created a Project nobody expected (today's composer#103 class of incident). | Cannot happen from CI after D5. From a laptop, it required `--create`. | D4, D5 | -## Open items this list surfaces +## How the open items were settled -1. D5: create the Project at connect time for the setup-PR path (A2, C2, D2). -2. A6: migration rule for existing single-string modules. -3. A7: backfill of `logicalId` on old Projects, or a one-time Console edit. -4. C3 and B5: plan output for identity changes below the root, and whether it needs consent. -5. E3: the fate of `--name`. +Every item this list first left open was settled later in the same discussion: + +1. Console creates the Project at connect time for the setup-PR path (D5). +2. Existing single-string modules stop loading, with an error naming the two-field form. Composer is pre-1.0, so there is no deprecation period (D7). +3. A one-time platform script backfills `logicalId` on linked Projects before the display-name fallback is removed (D10). +4. An identity change below the root is a consent question, answered by `--rename` or `--delete`, using the ORM's consent model (D8). +5. `--name` is removed. Re-identifying the root is `--rename store:storefront`. diff --git a/.drive/projects/node-identity-and-consent/design-notes.md b/.drive/projects/node-identity-and-consent/design-notes.md index 8bc907f13..2b30e992d 100644 --- a/.drive/projects/node-identity-and-consent/design-notes.md +++ b/.drive/projects/node-identity-and-consent/design-notes.md @@ -40,7 +40,7 @@ if that proves necessary. The display-name fallback in container resolution is removed. *Why:* the fallback is what let a name match adopt an unrelated Project. -ADR-0051 already calls it a compatibility path. +The decision record "a node's address is its full path from the app name" already calls it a compatibility path. *Assumes:* D10 backfills old Projects first. @@ -118,8 +118,9 @@ a large piece of platform work. ### D10. Backfill `logicalId` on old Projects once A platform script sets `logicalId` from the link's reserved module name on -every linked Project that has none. A Project with neither is set by hand in -Console. +every linked Project that has none. A Project with neither a `logicalId` nor a +link has no automatic fix. Whether Console lets a user set one by hand is an +open question. ### Smaller consequences @@ -154,7 +155,7 @@ Console. obsolete representation of the system after the transition, when only the command performing it ever needed it. The hint goes to the command instead. - **A committed consent file bound to a plan hash.** The hash differs per - stage, so a production consent blocks every preview deploy on the branch. + environment, so a production consent blocks every preview deploy on the branch. ## Open items and accepted trade-offs @@ -180,7 +181,7 @@ find-only mode for `--create`. | Change | Repo | | --- | --- | -| `logicalId` and `name` on every node; single-string form refused; `--create`; ORM consent model; `--name` removed; ADR amending ADR-0006, ADR-0024, ADR-0051 | prisma/composer | +| `logicalId` and `name` on every node; single-string form refused; `--create`; ORM consent model; `--name` removed; a decision record amending the records on node naming, environment resolution and full-path addresses | prisma/composer | | Console creates the Project at connect for the setup-PR path; empty-Project cleanup on release; inspector reads the hoisted constant; generated `module.mjs` uses the two-field form; backfill script | prisma/pdp-control-plane | | Nothing required. Close PR 355. | prisma/cloud-deploy-action | | Scoped CI tokens; Console approval of consent | platform, later | diff --git a/.drive/projects/node-identity-and-consent/plan.md b/.drive/projects/node-identity-and-consent/plan.md index 50b680631..8678f8bf5 100644 --- a/.drive/projects/node-identity-and-consent/plan.md +++ b/.drive/projects/node-identity-and-consent/plan.md @@ -10,7 +10,7 @@ Handoff plan. The slices below are the phases from the design, in the order they ### Phase 2 — Separate identity from label (Composer, then control plane) -Every node definition takes `logicalId` (required) and `name` (optional); Load uses `logicalId` for the address; the single-string form fails to load with an error naming the fix; `--name` is removed from `prisma deploy`; guides, examples and the core-concepts skill move to the two-field form; a new decision record states the model and amends the records on node naming, stage resolution and node addresses. Then the control plane: the setup-PR generator writes the two-field root with a hoisted `logicalId` constant and a one-line comment, and the module-file inspector reads that constant. One Composer release. +Every node definition takes `logicalId` (required) and `name` (optional); Load uses `logicalId` for the address; the single-string form fails to load with an error naming the fix; `--name` is removed from `prisma deploy`; guides, examples and the core-concepts skill move to the two-field form; a new decision record states the model and amends the records on node naming, environment resolution and node addresses. Then the control plane: the setup-PR generator writes the two-field root with a hoisted `logicalId` constant and a one-line comment, and the module-file inspector reads that constant. One Composer release. - **Builds on:** `deployment-identity` in production. - **Hands to:** phase 3 — Composer can tell a label edit from an identity edit. diff --git a/.drive/projects/node-identity-and-consent/spec.md b/.drive/projects/node-identity-and-consent/spec.md index acf0e1ad5..f3da8fede 100644 --- a/.drive/projects/node-identity-and-consent/spec.md +++ b/.drive/projects/node-identity-and-consent/spec.md @@ -53,10 +53,12 @@ CI tokens scoped to the Project the repo is connected to, then Console approval - [ ] A push-to-deploy run whose plan would delete a database fails closed and names the flags; no row changes. - [ ] A fresh Console connect writes the two-field root with a hoisted `logicalId` constant, and connecting a repo that brings its own two-field config reads the identifier correctly. - [ ] A CI token minted for a connected repo cannot read or write another Project in the workspace. -- [ ] A new Composer decision record states the two-field model and the consent model, and amends the records on node naming, stage resolution and node addresses. The guides, examples and the core-concepts skill use the two-field form throughout. +- [ ] A new Composer decision record states the two-field model and the consent model, and amends the records on node naming, environment resolution and node addresses. The guides, examples and the core-concepts skill use the two-field form throughout. # Open questions +- What the identity field on a child's definition is called. This design was settled before the decision record "a node's address is its full path from the app name" landed. That record makes the full address (`store.catalog`) the platform `logicalId`. So on a child, the value an author writes (`'catalog'`) is one segment of the `logicalId`, not the `logicalId` itself; only on the root are they equal. Either the field keeps the name `logicalId` and the docs explain that a child's `logicalId` is the full path built from it, or it gets a name for the segment. This needs a decision before phase 2 starts. + - Whether Console approval (phase 4b) is built in this project or spun out once scoped tokens exist. Decide at phase 4 planning. - Whether the plan output for an identity change below the root needs its own flag beyond the consent question. Decide in phase 3 slice planning. @@ -67,6 +69,6 @@ CI tokens scoped to the Project the repo is connected to, then Console approval - Worked example of the two-field model: [assets/logicalid-walkthrough.md](assets/logicalid-walkthrough.md). - Predecessor project: `.drive/projects/deployment-identity/`. - ORM consent model: prisma/orm `docs/CLI Style Guide.md`, section "Destructive operation confirmation". The two ORM changes that introduced it are "migration plan asks before a lossy type change" (commit 07a7bc06) and "migration plan requires consent for destructive auto-baselines" (commit c5a8b5ad). -- Composer decision records, in `docs/design/90-decisions/`: ADR-0006 "every node is named", ADR-0024 "a stage is a deploy-time environment resolved to Project and Branch", ADR-0050 "Composer runs as prisma deploy and prisma dev", ADR-0051 "a node's address is its logical ID on the platform". +- Composer decision records, in `docs/design/90-decisions/`: ADR-0006 "every node is named", ADR-0024 "a stage is a deploy-time environment resolved to Project and Branch", ADR-0050 "Composer runs as prisma deploy and prisma dev", ADR-0057 "a node's address is its full path from the app name" (it superseded ADR-0051), and ADR-0059 "a place the app runs is called an environment, not a stage or a branch". - Control plane, prisma/pdp-control-plane: ADR-010 "a connected repository is the trust boundary for CI deploys", ADR-011 "project slug as the deploy-target identity", `projects/branch-topology/spec.md`, `projects/composer-import/spec.md`, and `docs/compute/GitHubImport.md`. - Linear: Node identity and consent (Terminal team).