diff --git a/.drive/projects/deployment-identity/design-notes.md b/.drive/projects/deployment-identity/design-notes.md new file mode 100644 index 000000000..4ea6f33db --- /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: 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 # finds the Project by logicalId, or refuses +prisma deploy --create # finds it, or creates it 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 + +- The whole design, including the parts this project does not deliver: [node-identity-and-consent/design.md](../node-identity-and-consent/design.md). diff --git a/.drive/projects/deployment-identity/plan.md b/.drive/projects/deployment-identity/plan.md new file mode 100644 index 000000000..850aa7222 --- /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 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. + +## 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..64a31e875 --- /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"), 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. + +# 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 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 --name demo-42` example still works, with `--create` on its first run (`--name` itself is removed later, by `node-identity-and-consent`). +- [ ] 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 + +- The whole design, of which this project is part 1: [node-identity-and-consent/design.md](../node-identity-and-consent/design.md). +- 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-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/design.md b/.drive/projects/node-identity-and-consent/design.md new file mode 100644 index 000000000..800325455 --- /dev/null +++ b/.drive/projects/node-identity-and-consent/design.md @@ -0,0 +1,174 @@ +# Node identity and consent + +Status: proposed design. Owner: Terminal (Composer). Platform parts: Portal (the Prisma Data Platform control plane and Console). + +## The decision + +1. **Every node is declared with an `id`, and only the `id` gives it identity.** A node's `id` is the last segment of its address. The address is the path of `id`s from the app's root (`shop.catalog`), and it stays what it is today: the node's platform `logicalId`. `name` no longer gives a node its identity. +2. **A deploy finds its Project by the root's `id` and nothing else, and never creates a Project unless told to.** The fallback that matches a Project by its display name is removed. A deploy that matches no Project stops and tells you to pass `--create`. Console creates the Project when a repository is connected, so a connected repository's CI never needs `--create`. +3. **A deploy that would delete a database or a bucket asks first, the way Prisma ORM's `migration plan` and `db update` commands do.** The person deploying answers by naming each one: `--delete shop.catalog` to delete it, or `--rename shop.catalog:shop.catalogue` to keep it, with its data, under its new address. `--yes` never answers these questions. A non-interactive deploy, such as CI, stops and lists the flags that would answer them. +4. **`prisma deploy --name` is removed.** The root's `id` in the code is the only thing that names the app. +5. **Later, on the platform:** CI tokens that can reach only the Project their repository is connected to, then a way to answer a CI deploy's questions in Console. + +```ts +// Today: identity comes from `name`, which reads like a label. +export default appRoot('shop', ({ provision }) => { + const catalog = provision(postgres({ name: 'catalog', contract, config })); + provision(web, { deps: { db: catalog } }); // web.ts: compute({ name: 'web', … }) +}); + +// After: identity comes from `id`, and says so. +export default appRoot({ id: 'shop' }, ({ provision }) => { + const catalog = provision(postgres({ id: 'catalog', contract, config })); + provision(web, { deps: { db: catalog } }); // web.ts: compute({ id: 'web', … }) +}); +``` + +| Node | Address, and its platform `logicalId` | +| --- | --- | +| the root, `appRoot({ id: 'shop' })` | `shop`, which is also the Project's `logicalId` | +| `postgres({ id: 'catalog' })` | `shop.catalog` | +| `compute({ id: 'web' })` | `shop.web` | + +**Moving an existing app over changes nothing deployed.** Write each `id` with the string that forms the node's address segment today: the `id` passed to `provision()` if there is one, otherwise the node's `name`. The addresses then stay the same, and the next deploy changes nothing on the platform. + +## Why + +Today one string on each node, its `name`, decides three things at once: the node's identity, its Console label and its address. An edit that looks like relabelling changes identity: + +| Edit | What the author meant | What the next deploy does today | +| --- | --- | --- | +| a database `name` from `catalog` to `catalogue` | fix the spelling | deletes the `catalog` database with its data and creates an empty `catalogue` | +| the root from `shop` to `storefront` | a nicer name | creates a second, empty Project; `shop` stays behind with its data | +| the root from `shop` to `billing`, a name another repository already uses | copy and paste | deploys into the other repository's Project and overwrites its production environment | + +The last row was reported by a user. Two more things make it worse. When no Project's `logicalId` matches, Composer falls back to matching display names, so a rename can land in an unrelated Project. And a deploy that matches nothing quietly creates a Project. + +## The design + +### Declaring a node + +Every node has an `id`. You write it on every node you define with `appRoot`, `module`, `compute`, `postgres`, `rawPostgres` or a bucket. Shared modules, such as `auth()` and `email()`, come with an `id` of their own, so you write one only to change it. + +- **Format.** The root's `id` is the app name: letters, digits, `-` and `_`, starting with a letter or digit. Every other `id` is letters and digits only. These are the rules that apply today to the app name and to provision IDs. +- **Address.** A node's address is its parent's address, a dot, and its `id`. The root's address is its `id`. This is the address format that ships today; only the field it is read from changes. Two nodes under one parent cannot have the same `id`; the graph fails to load, as it does today when two addresses collide. +- **Using one definition twice.** `provision(node, { id })` still overrides the definition's `id` where it is provisioned. Its only use is placing the same definition twice in one graph, for example two `email()` modules with the `id`s `mail` and `alerts`. +- **No deprecation period.** A constructor given `name` and no `id` fails to load, with an error that says to write `id`. Composer is not yet at 1.0. Whether `name` is removed entirely, or kept as a label beside `id`, is the first open question; under the recommendation it is removed, and a `name` field is a type error and a load error. +- **Platform names.** Every display name Composer writes on the platform is the node's address, as today (`shop.catalog`). + +### How a deploy finds its Project + +- The deploy looks up the Project whose `logicalId` equals the root's `id`. It never matches by display name. +- If no Project matches, the deploy stops before it creates or changes anything: + + ``` + 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, its Project already exists; check the connection. + ``` + +- `prisma deploy --create` finds the Project or creates it with that `logicalId`. `destroy` already only finds, and never creates. +- Changing the root's `id` therefore points the deploy at a different Project. The old Project is left exactly as it was, with its services still running and its databases and buckets untouched. Removing it is a separate `prisma project delete`. +- **Console creates the Project when a repository is connected,** with the app name the user chose as its `logicalId`, on every connect path. CI therefore always finds its Project. If the user removes the setup, or closes the setup pull request without merging it, Console deletes that Project, but only if nothing has been deployed into it. +- **Older Projects get a `logicalId` once.** When a repository is connected, Console stores the app name the user chose on the connection record. A platform script copies that stored name into the `logicalId` of every connected Project that has none. It refuses a value another Project in the same workspace already has, and reports those Projects for a person to fix. It runs before the Composer release that removes the display-name fallback, so no connected repository stops deploying. +- **Deploying into another Project on purpose still works.** If the root's `id` is changed to another existing Project's `logicalId`, the deploy goes into that Project without asking. The difference from today is that the change is explicit in the diff, not a side effect of a label edit or a display-name match. Stopping it needs the scoped CI tokens described below; until they ship, this case stays open. + +### Asking before data is lost + +Before it changes anything, a deploy works out its plan for the environment it targets. Every database or bucket the plan would delete is a question. That happens when: + +- the node was removed from the code, or +- its address changed, because its `id` changed or it moved into another module. + +Deleting a service, an environment variable or a database's credentials loses no data, so none of these is a question. The plan output still lists them. A changed root `id` is handled by `--create` above, because it names a different Project. + +Interactively, the deploy asks one question per database or bucket: + +``` +The database shop.catalog would be deleted with its data (environment: production). + Keep it under a new address: --rename shop.catalog: + Delete it: --delete shop.catalog +``` + +The answers follow the ORM's rules for destructive operations: + +- `--delete
` lets the deploy delete that database or bucket. +- `--rename :` keeps it. The new address must be a node of the same kind that this deploy's plan would otherwise create from nothing: a database for a database, a bucket for a bucket. If it is not, the deploy fails before changing anything and says why. With a valid `--rename`, Composer tells Alchemy, the deploy engine it runs, that the resources at the old address are now at the new one. This is the mechanism Composer already uses internally to move resources between address formats; here it is generated for this one deploy and never written into code. The data stays, and the row's `logicalId` becomes the new address. +- Because names and keys follow the address, a rename also gives the database's credentials and a bucket's access key new values. Composer's own services receive them in the same deploy, together with their `COMPOSER_` environment variables under new keys. Anything outside Composer that holds the old credentials stops working. +- `--yes` never answers a question, and there is no `--force`. +- A `--delete` or `--rename` flag that matches no question fails the deploy with `CLI.CONSENT_UNUSED`. +- A non-interactive deploy with unanswered questions changes nothing and fails with `CLI.CONSENT_REQUIRED`, exit code 2. The error lists each question and the flags that answer it. +- Composer's programmatic API, `@prisma/composer/control`, takes the same answers through a callback, as the ORM's programmatic API does. + +**Every environment answers for itself.** The question comes from that environment's plan, so each environment that holds the database asks once. An environment that never had it asks nothing. For example, renaming `catalog` to `catalogue` in a pull request: + +1. The pull request's preview environment `pr-42` was created after the change, so it never had `shop.catalog`. Its CI deploy asks nothing and creates `shop.catalogue`. +2. After merging, the production CI deploy would delete production's `shop.catalog`, so it fails with `CLI.CONSENT_REQUIRED` and changes nothing. +3. A person runs `prisma deploy --environment production --rename shop.catalog:shop.catalogue` from a laptop. Production's database keeps its rows under the new address. +4. Production's deploy state now matches the code, so later production CI deploys ask nothing. Deploy state is Composer's record, kept by the platform for each environment, of what it has deployed there. Any other long-lived environment that had `shop.catalog`, such as `staging`, needs the same one-time deploy. + +### What is removed + +- `name` as a source of identity, on every node. +- `prisma deploy --name`. To deploy a second copy of an app, change the root's `id` and deploy with `--create`. +- Matching a Project by display name. +- Creating a Project without `--create`. + +### Later, on the platform + +- **Scoped CI tokens.** The token a connected repository's CI receives can reach only that repository's Project. Today it can reach the whole workspace. +- **Answering CI's questions in Console.** The build record would carry the deploy's questions, Console would show them, and a person's answer would let CI run again with it. This needs scoped tokens first. + +## Requirements this design meets + +| Requirement | How the design meets it | +| --- | --- | +| An edit that looks like relabelling cannot change a node's identity. | Identity comes only from `id`, a field named for what it does. | +| A deploy never creates a Project, or reaches a Project through a display-name match, without saying so. | The Project is found by `logicalId` only; creating one needs `--create`; Console creates connected repositories' Projects. Deploying into another Project by writing its `logicalId` stays possible, and explicit, until scoped CI tokens ship. | +| No deploy deletes a database or bucket unless a person named it. | One question per database or bucket, answered only by `--delete` or `--rename`; `--yes` never answers. | +| Changing a node's identity can keep its data. | `--rename` moves the database or bucket to its new address in place. | +| CI never consents to data loss on its own. | Unanswered questions fail the run before anything changes. | +| A node has one identity, the same in code, CLI and platform. | The `id`s form the address, and the address is the platform `logicalId`. | +| Every connected repository keeps deploying through the change. | The backfill runs before the Composer release that removes the display-name fallback, and writing `id` with today's strings keeps every address. | +| Composer knows nothing about GitHub. | Nothing reads `GITHUB_*` variables or repository ids; the link between a repository and its Project lives on the platform. | + +## What this changes in the recorded design + +These Composer decision records are amended or superseded when the work lands. Until then they describe what ships. + +- [Every node is named; the root's name names the application](../../../docs/design/90-decisions/ADR-0006-every-node-is-named.md): a node's identity comes from `id`, not `name`, and `--name` is removed. +- [A node's address is its full path from the app name](../../../docs/design/90-decisions/ADR-0057-a-nodes-address-is-its-full-path-from-the-app-name.md): the address is the path of `id`s; the display-name fallback is removed; an address change no longer silently deletes a database or bucket. +- [A stage is a deploy-time environment resolved to Project and Branch](../../../docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md): the Project is found by `logicalId` only and created only with `--create`. +- [Composer runs as `prisma deploy` and `prisma dev`](../../../docs/design/90-decisions/ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): `deploy` loses `--name` and gains `--create`, `--delete` and `--rename`. + +On the platform, the control plane's decision that made the root module's name the Project's `logicalId` is completed: the display-name fallback it kept for older Projects goes. + +## Delivery order + +Each part ships on its own. + +| Part | What | Where | +| --- | --- | --- | +| 1 | Find the Project by `logicalId` only; `--create`; Console creates the Project at connect; the backfill. No change to how modules are written. | Console and backfill first, then Composer. Linear project "Deployment identity". | +| 2 | `id` on every node; `name` no longer gives identity; `--name` removed. Console generates the setup pull request's module file and reads the app name from a repository's own module file; both switch to `id`. One release, because it breaks every module. | Composer, then Console. | +| 3 | Questions before a database or bucket is deleted; `--delete` and `--rename`. | Composer. | +| 4 | Scoped CI tokens, then answering questions in Console. | Platform. | + +## Open questions + +1. **Does a node keep `name` as a Console label?** If it did, `name` would set the row's display name and nothing else, and leaving it out would leave the display name alone, so a rename done in Console would survive. Recommendation: no. Every platform name stays the node's address, as it is today. A separate label would be a second name for the node, and relabelling is already harmless once `id` alone carries identity. +2. **How does a Project with no `logicalId` and no connected repository get one?** The backfill has nothing to copy. Console could let a user set it by hand. +3. **Can renaming the root keep its Project?** `--rename shop:storefront` would need the platform to change a Project's `logicalId`. The Management API's Project update accepts only the name and settings today. Until it accepts `logicalId`, changing the root's `id` means a new Project with `--create`. + +## Alternatives considered + +- **Check the GitHub repository id inside Composer.** Makes Composer depend on GitHub, and re-checks at deploy time what the platform already decided when the repository was connected. +- **Have CI's token exchange return the app name, and the deploy action pass `--name`.** Locks CI to its Project, but the identity disappears from the repository: a person reading the module file cannot tell which Project it deploys to, and a local deploy and a CI deploy of the same file can reach different Projects. +- **A platform-generated root identity**, either an opaque id or a readable name with a random suffix. An opaque id in user code breaks when the Project is deleted or the repository is forked. A random suffix invites someone to "tidy" it, which creates a new Project. +- **A `logicalId` field on each node** holding just that node's segment. The platform `logicalId` is the full address, so the same word would mean two different strings. +- **Default the `id` from `name`.** Brings back the problem one level down: a label edit changes identity again. +- **CI passes `--create` on every deploy.** The flag would then mean nothing in CI. And the generated workflow would only work with Composer versions that accept `--create`, so every connected repository would have to upgrade before its workflow changed. +- **Keep the display-name fallback, with a warning.** The fallback is what lets a rename adopt an unrelated Project. The backfill removes the need for it. +- **Consent written into the code**, such as rename declarations written by the author or Terraform's `moved` blocks. The ORM prototyped and rejected this: once the change has happened, the code still describes a system that no longer exists, when only the command making the change ever needed the information. The rename declarations Composer generates for its own address-format migration are not consent: the author never writes them, and they describe Composer's internal names. +- **A committed consent file tied to a plan hash.** The plan differs per environment, so a consent for production would block every preview deploy on the branch. +- **Ask about services too.** Deleting a service loses no data, so a question would be noise; the plan output lists it instead. 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..257339515 --- /dev/null +++ b/.drive/projects/node-identity-and-consent/plan.md @@ -0,0 +1,37 @@ +# Project Plan — node-identity-and-consent + +**Spec:** [spec.md](spec.md) · **Design:** [design.md](design.md) + +Handoff plan. Each part below is shipped on its own; the team picking it up splits it into slices. Builds on `deployment-identity`, which must be in production first. Linear project: [Node identity and consent](https://linear.app/prisma-company/project/node-identity-and-consent-9ec5f9f522c0). + +## Part 2 — `id` on every node (Composer, then Console) + +Every node constructor takes a required `id`; Load builds addresses from `id`s; a node declared with `name` and no `id` fails to load, naming `id`; `prisma deploy --name` is removed; guides, examples and the agent skill use `id`; the decision records listed in the design are amended. Then Console: the setup pull request writes `id`, and the module reader reads it. One Composer release. + +- **Needs first:** `deployment-identity` in production, and an answer to the design's first open question. +- **Hands to part 3:** Composer can tell an identity change from any other edit. + +## Part 3 — Ask before a database or bucket is deleted (Composer) + +Before applying, the deploy computes its plan; each database or bucket it would delete is a question answered by `--delete
` or `--rename :`, following the ORM's consent rules; non-interactive runs fail with `CLI.CONSENT_REQUIRED`. `--rename` writes Alchemy rename declarations for the run and updates the row's `logicalId` in place, which the platform already allows for services, databases and buckets. `--delete` may ship first. + +- **Needs first:** part 2. +- **Hands to part 4:** a CI deploy that needs answers fails with questions the platform could carry. + +## Part 4 — Platform (control plane) + +4a. CI tokens scoped to the connected repository's Project. 4b. Console shows a CI deploy's questions and re-runs it with the answers. + +- **Needs first:** 4a nothing beyond `deployment-identity`; 4b needs 4a and part 3. + +## Sequencing + +- In order: 2, then 3, then 4b. +- In parallel: 4a alongside 2 and 3. + +## Close-out (required) + +- [ ] Verify every DoD condition in [spec.md](spec.md). +- [ ] Move the long-lived design into a Composer decision record under `docs/design/90-decisions/`. +- [ ] Remove 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..b3a6e1e61 --- /dev/null +++ b/.drive/projects/node-identity-and-consent/spec.md @@ -0,0 +1,58 @@ +# Purpose + +Make every change to a Composer node's identity deliberate, and make every deploy that would delete a database or bucket ask a person first. The design is in [design.md](design.md); this spec is the project contract the slices are planned against. + +# At a glance + +- Every node is declared with a required `id`, the last segment of its address. `name` no longer gives identity. `prisma deploy --name` is removed. +- A deploy that would delete a database or bucket asks one question per database or bucket, answered by `--delete
` or `--rename :`, following the ORM's rules for destructive operations. Non-interactive runs fail with `CLI.CONSENT_REQUIRED` and change nothing. +- On the platform, later: CI tokens scoped to one Project, then answering a CI deploy's questions in Console. + +This project builds on `deployment-identity`, which makes a deploy find its Project by `logicalId` only and create one only with `--create`. + +# Non-goals + +- Defaulting `id` from `name`. +- Consent written into the code by the author, or a committed consent file. +- Questions about services, environment variables or connections, which hold no data. +- A deprecation period for `name` as identity. +- Any change to the deploy action or the generated workflow in parts 2 and 3. + +# Place in the larger world + +- Composer: every node constructor, Load (which builds each address), `prisma deploy`, and the plan step before a deploy changes anything. +- The ORM's CLI Style Guide, "Destructive operation confirmation": the consent rules to follow exactly, including `CLI.CONSENT_REQUIRED`, `CLI.CONSENT_UNUSED` and the control API's answer callback. +- Control plane: Console's setup pull request generator and its reader of the module file must write and read `id`. Part 4 narrows the CI token the control plane mints today for a whole workspace. + +# Cross-cutting requirements + +The requirements table in [design.md](design.md#requirements-this-design-meets) is the contract. In particular: + +- One identity per node: the `id`s form the address, and the address is the platform `logicalId`. +- No code path matches a platform row by display name. +- No deploy deletes a database or bucket without a statement naming it; `--yes` never answers. +- Composer stays unaware of GitHub. + +# Transitional-shape constraints + +- Part 2 lands in one Composer release, because it breaks every module. Console's setup files and module reader ship before or with it. +- In part 3, `--delete` may ship before `--rename`. Until `--rename` ships, a changed address of a database or bucket is answered only by `--delete`. + +# Project DoD + +- [ ] A module declared with `id` deploys; a node declared with `name` and no `id` fails to load with an error saying to write `id`. +- [ ] Against a real Project: changing a database's `id` and deploying without a statement fails with `CLI.CONSENT_REQUIRED` and changes nothing; with `--rename` the same database keeps its rows under the new `logicalId`; with `--delete` it is deleted and a new one created. +- [ ] A push-to-deploy run that would delete a database fails and names the flags; no row changes. +- [ ] Console's setup pull request writes `id`, and Console reads `id` from a repository that brings its own module. +- [ ] A connected repository's CI token cannot read or write another Project in the workspace. +- [ ] The decision records listed in [design.md](design.md#what-this-changes-in-the-recorded-design) are amended or superseded; the guides, examples and the agent skill use `id` throughout. + +# Open questions + +See [design.md](design.md#open-questions). None blocks part 2 except the first (whether `name` survives as a label), which decides part 2's API. + +# References + +- [design.md](design.md) +- prisma/orm `docs/CLI Style Guide.md`, "Destructive operation confirmation". +- Linear: "Node identity and consent" (Terminal team).