feat: adopt the upstream alchemy/Prisma provider - #197
Conversation
Summary by CodeRabbit
WalkthroughThis change adopts upstream 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commit: |
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/design/05-prisma-cloud/pdp-data-model.md (1)
82-89: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winAlign all deployment-propagation documentation with the shipped replacement mechanism.
These sections describe one stale model: deployment replacement occurs only when artifact content changes and environment changes do not trigger replacement. The shipped lowering uses a fresh generation path and replaces every deployment on every deploy.
docs/design/05-prisma-cloud/pdp-data-model.md#L82-L89: state that every deploy creates a replacement deployment under the current stopgap.docs/design/10-domains/core-model.md#L725-L737: remove the claim that environment changes are not propagated.docs/design/10-domains/core-model.md#L1096-L1100: usealwaysRedeployArtifactPath(artifact.path)in the example.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/design/05-prisma-cloud/pdp-data-model.md` around lines 82 - 89, Align all three deployment-propagation documentation sites with the shipped replacement behavior: in docs/design/05-prisma-cloud/pdp-data-model.md lines 82-89, state that every deploy creates a replacement deployment under the current stopgap; in docs/design/10-domains/core-model.md lines 725-737, remove the claim that environment changes are not propagated; and in docs/design/10-domains/core-model.md lines 1096-1100, update the example to use alwaysRedeployArtifactPath(artifact.path).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.drive/projects/alchemy-provider-adoption/assets/composer-pr-body.md:
- Line 30: Update the “Full suites green” statement in the PR body to report
tests as non-green, preserving the exact 62/63 pass count and explicitly
identifying the known dev-emulators flake; keep the build and typecheck results
unchanged.
- Line 1: Add a level-one Markdown title before the existing prose in both
.drive/projects/alchemy-provider-adoption/assets/composer-pr-body.md (lines 1-1)
and .drive/projects/alchemy-provider-adoption/assets/upstream-pr-body.md (lines
1-1), ensuring each document begins with an H1 heading.
In @.drive/projects/alchemy-provider-adoption/design-notes.md:
- Around line 14-20: Update the provider model section around “One provider
(upstream’s)” to scope ownership to retained bucket resources: state that
upstream owns PostgreSQL and compute, while Composer continues composing
BucketProvider and BucketKeyProvider until the upstream release; preserve the
existing deploy/dev layer distinction.
In @.drive/projects/alchemy-provider-adoption/spec.md:
- Around line 150-151: Update .drive/projects/alchemy-provider-adoption/spec.md
lines 150-151 to replace the beta.66 open question with the resolved
alchemy@2.0.0-beta.67 decision. In docs/design/03-domain-model/glossary.md lines
378-387, update the surrounding beta.59 reference to alchemy@2.0.0-beta.67.
In `@docs/design/03-domain-model/layering.md`:
- Line 38: Update the Service mapping row in the layering documentation to
replace Platform with the Composer resource graph established by glossary.md and
ADR-0043: ordinary App and Deployment resources, including the existing bundle,
manifest, and endpoint relationship. Keep the Service and ingress description
unchanged.
In `@docs/design/10-domains/config-params.md`:
- Around line 82-83: Update the canonical ADR-0039 decision and index references
to use the renamed origin API consistently, replacing outdated
ComputeService.origin() and endpointDomain terminology or adding an explicit
supersession note that identifies the current API. Ensure this section and the
ADR index do not present conflicting APIs.
In `@docs/guides/running-locally.md`:
- Around line 49-54: Update the --fresh guidance in the local-running
documentation to state that it is safe only when local instances and data are
disposable, rather than always safe. Preserve the stale-framework-state
troubleshooting context while explicitly warning that using --fresh wipes local
data.
In `@gotchas.md`:
- Around line 303-305: Update the gotcha sentence describing Prisma.App and
Prisma.Database so it states that Prisma.App creates on the target Branch, while
Prisma.Database is created at project scope and then attached to the branch via
PATCH. Remove the claim that both resources create directly on the target
Branch.
In `@packages/1-prisma-cloud/0-lowering/local-target/src/postgres.ts`:
- Around line 41-59: Extract the duplicated DEV_TIMESTAMP, isRecord, and
projectIdOfInput definitions from postgres.ts and compute.ts into a shared
upstream-input module, exporting all three symbols. Remove the local copies and
import the shared symbols in both files, while leaving databaseIdOfInput in
postgres.ts unchanged and preserving existing behavior.
In `@packages/1-prisma-cloud/0-lowering/lowering/src/client.ts`:
- Around line 20-33: Discharge URL resolver errors in the exported layer around
managementApiBaseUrl and the ManagementClient effect before returning
managementClientLayer. Apply Layer.orDie or an equivalent catchAll so the
provided layer no longer exposes Config.ConfigError | Error to callers, while
preserving PrismaCredentials as its remaining requirement.
In `@packages/1-prisma-cloud/0-lowering/lowering/src/compute/always-redeploy.ts`:
- Around line 41-66: Add best-effort lifecycle cleanup for directories matching
deploy-<generation> in the artifact parent used by alwaysRedeployArtifactPath.
Run cleanup only after the new deployment succeeds, preserve the newly returned
path and any paths still referenced by provider state, and ignore cleanup
failures so deployment success is unaffected.
In
`@packages/1-prisma-cloud/0-lowering/lowering/src/state/__tests__/legacy-resources.test.ts`:
- Around line 110-130: Extract a generic provider-service factory helper for the
five duplicated factories, centralizing the Effect.Effect cast and PrismaClient
layer setup. Parameterize it by the resource service type, provider, provider
layer, and stub client, then update databaseService, connectionService, and the
other provider factories to delegate to it while preserving their existing
behavior.
In `@packages/1-prisma-cloud/0-lowering/lowering/src/state/legacy-resources.ts`:
- Around line 211-217: Update poisonKeyOf to return a poison key only when the
EnvironmentVariable row’s props still match the legacy shape containing
projectId and key; continue deriving the key from the row attributes or legacy
props as appropriate, and return undefined for poison keys on non-legacy props.
Keep the existing family and POISON_KEYS checks intact.
- Around line 330-331: Move the poisonKey check in the legacy resource migration
flow so the nested old-chain rewrite completes before retirePoisonRow is called,
preserving the migrated EnvironmentVariable shape for replaced poison rows.
Update the surrounding function that handles family/row migration and add a
regression test in the legacy-resources tests covering a replaced poison row and
its migrated old generation.
---
Outside diff comments:
In `@docs/design/05-prisma-cloud/pdp-data-model.md`:
- Around line 82-89: Align all three deployment-propagation documentation sites
with the shipped replacement behavior: in
docs/design/05-prisma-cloud/pdp-data-model.md lines 82-89, state that every
deploy creates a replacement deployment under the current stopgap; in
docs/design/10-domains/core-model.md lines 725-737, remove the claim that
environment changes are not propagated; and in
docs/design/10-domains/core-model.md lines 1096-1100, update the example to use
alwaysRedeployArtifactPath(artifact.path).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 550b0395-1264-43c7-9b23-1d43865c369c
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (83)
.drive/projects/alchemy-provider-adoption/assets/composer-pr-body.md.drive/projects/alchemy-provider-adoption/assets/upstream-pr-body.md.drive/projects/alchemy-provider-adoption/design-notes.md.drive/projects/alchemy-provider-adoption/plan.md.drive/projects/alchemy-provider-adoption/spec.mddocs/design/03-domain-model/glossary.mddocs/design/03-domain-model/layering.mddocs/design/05-prisma-cloud/alchemy-lowering.mddocs/design/05-prisma-cloud/pdp-data-model.mddocs/design/10-domains/config-params.mddocs/design/10-domains/core-model.mddocs/design/10-domains/local-dev.mddocs/design/90-decisions/ADR-0043-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.mddocs/design/90-decisions/README.mddocs/guides/deploying.mddocs/guides/running-locally.mdexamples/bucket/package.jsonexamples/cron/package.jsonexamples/pn-widgets/package.jsonexamples/storage/package.jsonexamples/store/package.jsonexamples/storefront-auth/package.jsongotchas.mdpackages/0-framework/1-core/core/package.jsonpackages/1-prisma-cloud/0-lowering/local-target/package.jsonpackages/1-prisma-cloud/0-lowering/local-target/src/__tests__/compute-scoped-env.test.tspackages/1-prisma-cloud/0-lowering/local-target/src/__tests__/postgres-instance-name-drift.test.tspackages/1-prisma-cloud/0-lowering/local-target/src/compute.tspackages/1-prisma-cloud/0-lowering/local-target/src/postgres.tspackages/1-prisma-cloud/0-lowering/local-target/src/providers.tspackages/1-prisma-cloud/0-lowering/lowering/package.jsonpackages/1-prisma-cloud/0-lowering/lowering/src/__tests__/ComputeService.test.tspackages/1-prisma-cloud/0-lowering/lowering/src/__tests__/Database.test.tspackages/1-prisma-cloud/0-lowering/lowering/src/__tests__/EnvironmentVariable.test.tspackages/1-prisma-cloud/0-lowering/lowering/src/__tests__/ServiceKey.test.tspackages/1-prisma-cloud/0-lowering/lowering/src/buckets/Bucket.tspackages/1-prisma-cloud/0-lowering/lowering/src/buckets/BucketKey.tspackages/1-prisma-cloud/0-lowering/lowering/src/client.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/ComputeService.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/Deployment.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/EnvironmentVariable.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/__tests__/always-redeploy.test.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/__tests__/deployment-edge.test.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/always-redeploy.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/artifact.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/deployment-edge.tspackages/1-prisma-cloud/0-lowering/lowering/src/credentials.tspackages/1-prisma-cloud/0-lowering/lowering/src/exports/compute.tspackages/1-prisma-cloud/0-lowering/lowering/src/exports/index.tspackages/1-prisma-cloud/0-lowering/lowering/src/exports/postgres.tspackages/1-prisma-cloud/0-lowering/lowering/src/postgres/Connection.tspackages/1-prisma-cloud/0-lowering/lowering/src/postgres/Database.tspackages/1-prisma-cloud/0-lowering/lowering/src/postgres/Project.tspackages/1-prisma-cloud/0-lowering/lowering/src/providers.tspackages/1-prisma-cloud/0-lowering/lowering/src/state/__tests__/legacy-resources.test.tspackages/1-prisma-cloud/0-lowering/lowering/src/state/layer.tspackages/1-prisma-cloud/0-lowering/lowering/src/state/legacy-resources.tspackages/1-prisma-cloud/0-lowering/lowering/src/state/service.tspackages/1-prisma-cloud/0-lowering/lowering/src/state/transient.tspackages/1-prisma-cloud/0-lowering/lowering/tsdown.config.tspackages/1-prisma-cloud/1-extensions/target/package.jsonpackages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.tspackages/1-prisma-cloud/1-extensions/target/src/__tests__/generated-param.test.tspackages/1-prisma-cloud/1-extensions/target/src/__tests__/pg-warm-resource.test.tspackages/1-prisma-cloud/1-extensions/target/src/__tests__/pn-migration-resource.test.tspackages/1-prisma-cloud/1-extensions/target/src/__tests__/s3-credentials.test.tspackages/1-prisma-cloud/1-extensions/target/src/control/extension.tspackages/1-prisma-cloud/1-extensions/target/src/descriptors/compute.tspackages/1-prisma-cloud/1-extensions/target/src/descriptors/postgres.tspackages/1-prisma-cloud/1-extensions/target/src/descriptors/prisma-next.tspackages/1-prisma-cloud/1-extensions/target/src/descriptors/shared.tspackages/1-prisma-cloud/1-extensions/target/src/param.tspackages/1-prisma-cloud/1-extensions/target/src/preflight.tspackages/1-prisma-cloud/1-extensions/target/src/secret.tspackages/1-prisma-cloud/1-extensions/target/src/serializer.tspackages/9-public/composer-prisma-cloud/package.jsonpackages/9-public/composer/package.jsonpatches/alchemy@2.0.0-beta.67.patchpnpm-workspace.yamlscripts/ci-cleanup-utils.tstest/integration/package.jsontest/integration/test/local-dev.integration.tswebsite/package.json
💤 Files with no reviewable changes (11)
- packages/1-prisma-cloud/0-lowering/lowering/src/exports/postgres.ts
- packages/1-prisma-cloud/0-lowering/lowering/src/postgres/Connection.ts
- packages/1-prisma-cloud/0-lowering/lowering/src/postgres/Database.ts
- packages/1-prisma-cloud/0-lowering/lowering/src/postgres/Project.ts
- packages/1-prisma-cloud/0-lowering/lowering/src/tests/Database.test.ts
- packages/1-prisma-cloud/0-lowering/lowering/src/compute/ComputeService.ts
- packages/1-prisma-cloud/0-lowering/lowering/src/tests/EnvironmentVariable.test.ts
- packages/1-prisma-cloud/0-lowering/lowering/tsdown.config.ts
- packages/1-prisma-cloud/0-lowering/lowering/src/compute/EnvironmentVariable.ts
- packages/1-prisma-cloud/0-lowering/lowering/src/tests/ComputeService.test.ts
- packages/1-prisma-cloud/0-lowering/lowering/src/compute/Deployment.ts
|
Re the outside-diff finding in the CHANGES_REQUESTED review (deployment propagation docs): done in ce6c2cd. Both prose sites now describe the mechanism this branch ships — an environment change reaches the running app because the deployment edge replaces the deployment — instead of claiming env-var changes never propagate:
|
| export function mangleExtensionId(extensionId: string): string { | ||
| return extensionId |
There was a problem hiding this comment.
What do you mean "mangle"?
There was a problem hiding this comment.
Renamed in a9464cd: mangleExtensionId is now envVarSafeExtensionId, and its comment states the literal transformation — the extension id uppercased with every non-alphanumeric run replaced by one underscore, e.g. @prisma/composer-prisma-cloud -> PRISMA_COMPOSER_PRISMA_CLOUD.
| /** | ||
| * Carries what an extension's deploy preflight learned from the CLI process | ||
| * into the alchemy process — the same two-process problem, and the same | ||
| * channel, as resolved containers (ADR-0037, container-transport.ts). | ||
| * | ||
| * Preflight runs in the CLI parent, because it is the step that talks to the | ||
| * platform. Alchemy then runs as a child process against the generated stack | ||
| * file, which re-imports the app config from scratch: every extension factory | ||
| * is called again, with none of the parent's state. Anything preflight learned | ||
| * that the lowering needs is therefore gone unless it is transported, and env | ||
| * vars are the only channel between the two processes. So the CLI writes each | ||
| * extension's preflight payload into one env var, and the extension reads its | ||
| * own var back in the alchemy process. The framework owns the vars; it never | ||
| * reads their contents. | ||
| * | ||
| * An extension must never put a SECRET VALUE in a payload: the alchemy child's | ||
| * environment is not a secret store, and the payload is not encrypted. Carry | ||
| * metadata (e.g. when a platform variable was last written), never values. | ||
| */ |
There was a problem hiding this comment.
Don't put giant comments in file headers. Re-review all comments in the diff - don't be so verbose. Nobody needs all this text and it will never be read.
There was a problem hiding this comment.
Swept the whole diff in a9464cd, not just this file. This header went from 19 lines to 6 (the transport in one sentence plus the no-secrets rule). The other big blocks were cut the same way: deploy-fingerprint.ts 85 -> 19, legacy-resources.ts 61 -> 14, providers.ts 24 -> 13, deployment-edge.ts 34 -> 12, pointer-timestamps.ts 18 -> 8, the local-target and descriptor inline blocks, and the largest test-file headers. Net: the PR now adds ~235 fewer comment lines (982 -> 747 across packages/), and what remains states constraints the code cannot express.
| export function isRecord(value: unknown): value is Record<string, unknown> { | ||
| return typeof value === 'object' && value !== null; | ||
| } |
There was a problem hiding this comment.
Pretty sure this util already exists
There was a problem hiding this comment.
It did exist — effect ships Predicate.isObject (non-null, non-array object guard). a9464cd deletes the local isRecord and uses Predicate.isObject in upstream-attributes.ts, compute.ts, and postgres.ts. (The workspace has several other private module-local isRecord copies predating this PR — deployment-summary.ts, load-config.ts, container.ts, pointer-timestamps.ts — left alone here as out of this PR's scope.)
|
|
||
| /** A Prisma **Object Store bucket** inside a project. */ | ||
| export const Bucket = Resource<Bucket>('Prisma.Bucket'); | ||
| export const Bucket = Resource<Bucket>('PrismaComposer.Bucket', { aliases: ['Prisma.Bucket'] }); |
There was a problem hiding this comment.
Let's stop using magic strings. Extract the 'PrismaComposer.Bucket' id into a const
There was a problem hiding this comment.
Also why do we need the alias?
There was a problem hiding this comment.
Done in a9464cd: BUCKET_TYPE_ID / BUCKET_LEGACY_TYPE_ID in Bucket.ts and BUCKET_KEY_TYPE_ID / BUCKET_KEY_LEGACY_TYPE_ID in BucketKey.ts, used in both the type literal (typeof) and the Resource() call, so the id is written once per resource. Those two are the only resources this PR defines with the doubled-literal pattern; the pre-existing PrismaCloud.* resources (ServiceKey, PgWarm, PnMigration, S3Credentials, GeneratedParam) share the pattern but are untouched by this PR, so I left them for a separate cleanup.
| /** A **bucket access key** for a Prisma Object Store bucket — yields the S3 credentials. */ | ||
| export const BucketKey = Resource<BucketKey>('Prisma.BucketKey'); | ||
| export const BucketKey = Resource<BucketKey>('PrismaComposer.BucketKey', { | ||
| aliases: ['Prisma.BucketKey'], |
There was a problem hiding this comment.
Why do all these resoruces have aliases?
There was a problem hiding this comment.
Aliases are alchemy's native mechanism for renamed resource types: provider lookup also resolves state rows persisted under a listed old type-id. This PR renames Composer's own resources from Prisma.* to PrismaComposer.* (freeing the Prisma.* namespace for upstream's classes), so any already-deployed stage has bucket rows persisted as Prisma.Bucket / Prisma.BucketKey — without the alias those rows would find no provider and the deploy would fail at plan time. Both aliases exist for exactly that population; none is unnecessary, so none was removed. They can be dropped once no pre-rename state remains.
| * it, replace this — pass the canonical `artifact.path` verbatim again and put | ||
| * this fingerprint on `redeployOn`. The one call site is | ||
| * `descriptors/compute.ts`'s deploy hook. | ||
| */ |
There was a problem hiding this comment.
THIS IS AN 85 LINE COMMENT!
There was a problem hiding this comment.
Cut in a9464cd: 85 lines -> 19. What remains is only what the code cannot say — why the environment rides artifactPath at all, the no-secrets rule for the hash, the accepted blind spot (a value re-minted in place by the same resources), and the redeployOn replacement seam. The removed narration (diff-collapse mechanics, the three rotation flows, the dev caveat) lives in the tests that pin those behaviors.
| import { type ManagementApiClient, ManagementClient } from './client.ts'; | ||
| import { callCreateOnly, type PrismaApiError } from './http.ts'; | ||
|
|
||
| const POISON_VALUE = '-'; |
There was a problem hiding this comment.
It's really concerning these are "poison keys" and "poison values". Rename all of this without the word poison. You've way overfitted to your own invented vocabulary
There was a problem hiding this comment.
Renamed throughout in a9464cd, in plain terms of what the code does: it CLAIMS the platform's reserved DATABASE_URL / DATABASE_URL_POOLED keys with a placeholder value before the platform can seed them. The module is now database-url-claim.ts exporting claimDatabaseUrlKeys, with PLACEHOLDER_VALUE ("-") and RESERVED_DATABASE_URL_KEYS; the legacy state rows older versions persisted for those keys are "legacy claim rows" (retireDatabaseUrlClaimRow, CLAIMED_DATABASE_URL_KEYS, marker id dev:legacy-claim-<key>); tests, comments, and the docs (alchemy-lowering.md, ADR) follow. Pre-existing uses of "poison" elsewhere on main (module-import poisoning in control-import.test, name squatting in dev-emulators) are different senses and untouched.
…ign notes Adopt the upstream alchemy/Prisma provider for the six overlapping resources, keep Composer emulators local, contribute buckets + the generic Postgres state store upstream. Slices TML-3154/3155/3156. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…e-ids, aliases Bump alchemy 2.0.0-beta.59 -> .67 (13 pins) + forced effect beta.100 train. Rename the provider collection tag and the eight resource type-ids from Prisma.* to PrismaComposer.* so they cannot collide with the upstream alchemy/Prisma provider; old ids wired as aliases so existing state rows resolve (beta.67 ResourceOptions.aliases). One-line pnpm patch for upstream Aliases typing under exactOptionalPropertyTypes; Schedule.both removal handled with Schedule.upTo (during is inverted in beta.100, verified by probe). Part of alchemy-provider-adoption (TML-3154). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Delete Composer's Project/Database/Connection resources; register upstream's classes in the PrismaComposer collection over a profile-store-free PrismaEnvironment (PRISMA_SERVICE_TOKEN + shared managementApiBaseUrl resolver honored identically by our SDK client). Descriptors bind directConnectionString explicitly; branch stages create with branchId and a generated physical name (upstream's reconcile reverts external attach/rename, so create-then-PATCH is not viable); production keeps explicit names. Legacy state rows migrate on read in the hosted store (type-ids + attribute shapes), proven against upstream's real diff/read/reconcile incl. a no-rotation guard; the one-time branch-stage rename + default-connection rotation is documented in docs/guides/deploying.md. Local-target providers rebind the emulators to upstream's classes. Part of alchemy-provider-adoption (TML-3154). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… alternatives Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Replace ComputeService/Deployment/EnvironmentVariable with upstream App/Deployment/EnvironmentVariable (not composite Compute: the COMPOSER_*_ORIGIN self-edge needs the App created before env rows, and Compute owns env rows in a map our per-key state cannot migrate into). The env->deployment ordering edge rides the app prop as an Output (deployment-edge.ts) — riding artifactPath would leave the diff unresolved for brand-new env rows and silently skip code deploys; proven by tests driving alchemy's real Output machinery. Legacy compute rows migrate on read incl. poison-row neutralization; the platform DATABASE_URL is no longer overwritten (system-managed; authoring-side ban remains). Local target rebinds the emulators to upstream classes. Known regressions recorded in design notes: env value changes no longer redeploy (upstream ask filed) and App delete retry budget is ~3.75s (upstream ask filed). Part of alchemy-provider-adoption (TML-3155). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…m Alchemy provider Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…tion docs Legacy poison DATABASE_URL rows are retired with removalPolicy retain: the engine drops the state row, calls no API, and prints "retained" — the previous "deleted" was false (the platform variable survives, still holding the "-" placeholder on migrated stages). deploying.md, alchemy-lowering.md and the design notes now say so, with the optional manual cleanup calls. Part of alchemy-provider-adoption (TML-3155). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Restore the pre-swap guarantee that a deploy ships what you declared: the deploy hook hard-links the content-addressed artifact into a per-deploy-generation path, so upstream Deployment plans a replace every deploy (the updatedAt route is impossible: not in stables, and the env diff plans update every deploy, so plan-time values are unresolved). No secret material in state. The app ordering edge is untouched. Cost is the pre-swap profile: one deployment replacement per service per deploy, until the pinned alchemy version includes Deployment.redeployOn — the swap is one edit at the marked seam. Part of alchemy-provider-adoption (TML-3155). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…s only Grounding code first, decision led, alternatives last. Removed transient project/process references and corrected two statements the implementation had already overtaken (env changes now always ship via per-generation artifact paths; upstream dev mode is dual registration, not an option we pass). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
createDatabase: false removes the default database but not the variable: the platform heals a missing DATABASE_URL template on the first Compute deploy from any ready database on the Project (verified in the control plane: healDefaultDatabaseUrl via materializeDeployEnv). The authoring-side name ban is the line that holds. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
application.provision claims DATABASE_URL/DATABASE_URL_POOLED (production + preview, project-level) with "-" via create-only POSTs, skipping 409s. The platform build-runner self-heals a missing DATABASE_URL template on first deploy from any ready database, so without the claim a bypass reader quietly gets a live credential to one of the app's own databases; with it, direct readers fail loudly. The rows are never alchemy resources (nothing in state, upstream never owns them); legacy projects with platform-seeded rows 409 and no-op. Part of alchemy-provider-adoption (TML-3155). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The lowering doc and deploy guide describe the create-only claim, why it exists (the platform fills a missing DATABASE_URL from any ready database on first deploy), and that manual deletion is not useful — the claim or the platform recreates the row; a user-set value wins over both. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…h wording ADR index origin API naming (App.appEndpointDomain per ADR-0043), layering Service row, glossary beta pin, spec open question resolved, design-notes provider-scope claim, gotchas branch-attach fix line matches its cause, --fresh documented as data-destroying, and the two propagation passages now describe env changes reaching the running deployment. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…sed otherwise Replace the per-run generation path with a deploy fingerprint: the artifact hard-link directory is named from a hash of the service's environment material, so upstream's artifactPath comparison replaces exactly when the environment or artifact changed. The material is non-secret by construction (ADR-0042 rows carry literals and pointers); pointed platform variables contribute updatedAt metadata, read at preflight and carried across the CLI->alchemy process boundary on a new framework preflight-transport channel (the in-process implementation lost the timestamps at spawn — caught in review, now covered by a boundary-spanning test). Secret-bearing rows contribute wiring identity only; the accepted narrowing is recorded in the module. Docs updated to the shipped mechanism. Part of alchemy-provider-adoption (TML-3155); addresses PR 197 A11. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ribute helpers The compute and postgres provider families each carried their own byte-identical DEV_TIMESTAMP, isRecord and projectIdOfInput. They now import them from src/upstream-attributes.ts, so the 'local' project fallback and the stamped timestamp cannot drift between the two. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…hape Upstream's own EnvironmentVariable rows carry the same type-id as the legacy ones, so a key match alone would retire a live, upstream-managed DATABASE_URL variable from state on every read. poisonKeyOf now applies the same props-shape discriminator the rest of the module uses. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…retiring it The retirement returned early, so the displaced generation nested under `old` kept its legacy shape and reached the engine unmigrated. The old chain is rewritten first; the retirement now happens on the fully migrated row. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Integration fixes after rebasing onto main (PR #209 replaced the SQL state store with the platform state API): - legacy-resources.test.ts round-trip suite now drives the REAL hosted layer (stateLayerAgainst -> stock HTTP client -> on-read migration) against the in-process fake state API instead of the deleted SQL store and Postgres harness; the pure migration and provider acceptance tests are unchanged - the adoption ADR is renumbered ADR-0046 (main took 0043-0045); references updated - lockfile regenerated on main's pins (effect beta.103 constellation, @prisma/management-api-sdk ^1.57.0, no postgres) plus @effect/platform-node for the upstream providers Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
c434e9d to
813a2ec
Compare
…omments - rename mangleExtensionId to envVarSafeExtensionId; comments say the literal transformation - cut the oversized comments this PR added (deploy-fingerprint 85-line header, preflight-transport, legacy-resources, providers, deployment-edge, pointer-timestamps, local-target, extension, descriptors/compute, test headers) to the constraints the code cannot express - replace local-target's isRecord with effect's Predicate.isObject - extract the resource type-id strings in Bucket/BucketKey into consts (with the legacy alias ids named too) - rename the "poison" vocabulary: database-url-poison.ts becomes database-url-claim.ts (claimDatabaseUrlKeys, PLACEHOLDER_VALUE, RESERVED_DATABASE_URL_KEYS); legacy-resources' retirePoisonRow becomes retireDatabaseUrlClaimRow; tests and docs follow Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
There was a problem hiding this comment.
Actionable comments posted: 14
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.drive/projects/alchemy-provider-adoption/assets/composer-pr-body.md:
- Line 24: Update
.drive/projects/alchemy-provider-adoption/assets/composer-pr-body.md lines 24
and 42 to describe the current fingerprint-based deployment seam instead of
always-redeploy.ts: unchanged services are reused, while artifact or relevant
environment fingerprint changes replace deployments. Update
docs/guides/deploying.md lines 279-285 with the same behavior, explicitly
stating that unchanged services are reused and changed fingerprints trigger
replacement.
In @.drive/projects/alchemy-provider-adoption/design-notes.md:
- Around line 174-176: The destroyApp retry description must match its
exponential-backoff semantics: update the stated total waiting time to 7.75
seconds for five waits, or explicitly define whether “five attempts” includes
the initial call and count the corresponding waits. Keep the surrounding
deployment-draining and deletion behavior unchanged.
In @.drive/projects/alchemy-provider-adoption/spec.md:
- Around line 141-149: The specification’s adoption questions remain marked as
pending despite the implementation decisions. Update the “Compute vs
App+Deployment” entry to record the selected App + Deployment approach and the
“State migration mechanics” entry to record on-read state migration, marking
both resolved or moving their rationale into the completed-decisions section.
In `@docs/design/05-prisma-cloud/alchemy-lowering.md`:
- Around line 86-91: Update the resource inventory’s introductory wording and
column heading to identify these as upstream-owned resources lowered from
Alchemy/Prisma, rather than Composer-owned resources. Preserve the existing
resource mappings and terminology, while aligning the ownership language with
the upstream ownership statement and ADR-0046.
In
`@docs/design/90-decisions/ADR-0046-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.md`:
- Around line 147-154: Update the ADR section describing platform-managed
DATABASE_URL variables to clarify that Composer never overwrites existing
platform rows or tracks them, but may create DATABASE_URL and
DATABASE_URL_POOLED placeholder rows on fresh projects through create-only
operations.
- Around line 104-128: Update the ADR-0046 section title and text to describe
conditional deployment replacement: tracked artifact or environment-material
changes create a new deployment, while unchanged inputs reuse the existing
deployment and stable-identity secret reissues remain an accepted exception. In
docs/design/90-decisions/ADR-0046-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.md
lines 104-128, align the wording with the deploy fingerprint behavior. In
docs/design/90-decisions/README.md line 70, replace the claim that every deploy
replaces the deployment with this conditional behavior.
In `@docs/guides/deploying.md`:
- Around line 287-307: Rewrite the DATABASE_URL and DATABASE_URL_POOLED
documentation to remove all placeholder-claim, creation, recreation, and
platform-filler assertions. Describe that adoption performs no writes or
platform API calls, while existing legacy rows are removed from deploy state
with retained; state that fresh stages may receive platform template values, and
instruct services to use declared connections or an explicitly managed value
instead of relying on direct environment reads.
In `@gotchas.md`:
- Line 206: Update the workaround documentation to distinguish secret-bearing
environment rows from non-secret values: secret rotation under the same resource
identity does not alter the deployment fingerprint, so it requires an explicit
new version or redeploy action to reach the running service. Keep the existing
automatic fingerprint behavior for non-secret environment changes.
In `@packages/0-framework/1-core/core/src/__tests__/preflight-transport.test.ts`:
- Around line 39-48: Update the collision assertion in the test named “two ids
that map to the same var name fail loudly, naming both” to require the thrown
diagnostic to include both extension IDs, “acme.widgets” and “acme/widgets”,
while retaining the existing collision-message check.
In `@packages/0-framework/3-tooling/cli/src/operations/execute-deploy-destroy.ts`:
- Line 254: Update the deploy flow around the preflight loop and runAlchemy
call: build the environment record only after all preflight payloads have been
processed, catch duplicate extension-ID environment variable collisions, and
convert them to DEPLOY.PREFLIGHT_FAILED. Pass the validated record to runAlchemy
instead of invoking preflightEnv inline during stack writing.
In `@packages/1-prisma-cloud/0-lowering/local-target/src/compute.ts`:
- Around line 41-61: Update artifactSha256 to be asynchronous and replace
fs.statSync/openSync/readSync with promise-based file APIs, preserving the
existing SHA-256 digest and memoization behavior. Update its provider call site
to await artifactSha256 when computing artifactHash, ensuring hashing yields to
the event loop while retaining proper file-handle cleanup.
In
`@packages/1-prisma-cloud/0-lowering/lowering/src/compute/deploy-fingerprint.ts`:
- Around line 100-107: Update the artifact linking fallback around the
existsSync/linkSync flow to avoid writing directly to linked: copy artifactPath
to a uniquely named temporary path in the same directory, then atomically rename
it to linked. Handle an EEXIST from either the hard-link or final rename
operation as success, while preserving fallback behavior for other filesystem
errors.
In
`@packages/1-prisma-cloud/0-lowering/lowering/src/state/__tests__/legacy-resources.test.ts`:
- Around line 787-853: The round-trip suite currently covers only get; add a
test using runLayer that seeds a replaced legacy row via the service API and
reads it through getReplacedResources. Assert the returned row is migrated to
the upstream shape, including relevant resource attributes and preserved
Redacted values, so the wrapped replacement-read path is exercised.
In `@packages/1-prisma-cloud/1-extensions/target/src/serializer.ts`:
- Around line 518-524: Update the serializer return in the surrounding
serialization function to deduplicate the values from sentinels and sort them
deterministically before assigning secrets. Preserve the existing secret names
while ensuring duplicate bindings produce one entry and traversal order cannot
affect the result.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1c1efe59-83a3-4521-bbe7-dd010cbb5fc6
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (54)
.drive/projects/alchemy-provider-adoption/assets/composer-pr-body.md.drive/projects/alchemy-provider-adoption/design-notes.md.drive/projects/alchemy-provider-adoption/spec.mddocs/design/03-domain-model/glossary.mddocs/design/03-domain-model/layering.mddocs/design/05-prisma-cloud/alchemy-lowering.mddocs/design/05-prisma-cloud/pdp-data-model.mddocs/design/10-domains/core-model.mddocs/design/90-decisions/ADR-0046-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.mddocs/design/90-decisions/README.mddocs/guides/deploying.mddocs/guides/running-locally.mdgotchas.mdpackages/0-framework/1-core/core/src/__tests__/container-transport.test.tspackages/0-framework/1-core/core/src/__tests__/preflight-transport.test.tspackages/0-framework/1-core/core/src/container-transport.tspackages/0-framework/1-core/core/src/control/app-config.tspackages/0-framework/1-core/core/src/preflight-transport.tspackages/0-framework/3-tooling/cli/src/operations/execute-deploy-destroy.tspackages/0-framework/3-tooling/cli/src/run-alchemy.tspackages/1-prisma-cloud/0-lowering/local-target/src/compute.tspackages/1-prisma-cloud/0-lowering/local-target/src/postgres.tspackages/1-prisma-cloud/0-lowering/local-target/src/upstream-attributes.tspackages/1-prisma-cloud/0-lowering/lowering/package.jsonpackages/1-prisma-cloud/0-lowering/lowering/src/__tests__/database-url-claim.test.tspackages/1-prisma-cloud/0-lowering/lowering/src/buckets/Bucket.tspackages/1-prisma-cloud/0-lowering/lowering/src/buckets/BucketKey.tspackages/1-prisma-cloud/0-lowering/lowering/src/client.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/__tests__/deploy-fingerprint.test.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/__tests__/deployment-edge.test.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/artifact.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/deploy-fingerprint.tspackages/1-prisma-cloud/0-lowering/lowering/src/compute/deployment-edge.tspackages/1-prisma-cloud/0-lowering/lowering/src/container.tspackages/1-prisma-cloud/0-lowering/lowering/src/database-url-claim.tspackages/1-prisma-cloud/0-lowering/lowering/src/exports/compute.tspackages/1-prisma-cloud/0-lowering/lowering/src/exports/index.tspackages/1-prisma-cloud/0-lowering/lowering/src/http.tspackages/1-prisma-cloud/0-lowering/lowering/src/providers.tspackages/1-prisma-cloud/0-lowering/lowering/src/state/__tests__/legacy-resources.test.tspackages/1-prisma-cloud/0-lowering/lowering/src/state/layer.tspackages/1-prisma-cloud/0-lowering/lowering/src/state/legacy-resources.tspackages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.tspackages/1-prisma-cloud/1-extensions/target/src/__tests__/invariants.test.tspackages/1-prisma-cloud/1-extensions/target/src/__tests__/param.test.tspackages/1-prisma-cloud/1-extensions/target/src/__tests__/pointer-timestamps.test.tspackages/1-prisma-cloud/1-extensions/target/src/__tests__/preflight.test.tspackages/1-prisma-cloud/1-extensions/target/src/__tests__/secret.test.tspackages/1-prisma-cloud/1-extensions/target/src/control/extension.tspackages/1-prisma-cloud/1-extensions/target/src/control/pointer-timestamps.tspackages/1-prisma-cloud/1-extensions/target/src/descriptors/compute.tspackages/1-prisma-cloud/1-extensions/target/src/descriptors/shared.tspackages/1-prisma-cloud/1-extensions/target/src/preflight.tspackages/1-prisma-cloud/1-extensions/target/src/serializer.ts
| - **Foundation** — alchemy beta.59 → beta.67 (plus the forced effect beta.100 train). Our provider collection tag and remaining resource type-ids renamed to `PrismaComposer.*`; the old ids are aliases so existing state rows resolve. | ||
| - **Postgres family** — upstream `Project`/`Database`/`Connection` classes, driven by our own auth layer (`PrismaEnvironment` from `PRISMA_SERVICE_TOKEN`, no interactive profile store; one base-URL resolver shared with our SDK client). Branch stages create their database attached with a generated physical name (upstream correctly refuses explicit-name-plus-branch; verified against PDP source). `directConnectionString` is bound explicitly — upstream's `databaseUrl` is pooled-first. | ||
| - **Compute family** — upstream's low-level `App`/`Deployment`/`EnvironmentVariable`, not composite `Compute`: the `COMPOSER_*_ORIGIN` self-edge needs the App to exist before env rows, and `Deployment` has no build path at all (ADR-0005 by structure). The env→deployment ordering edge rides the deployment's `app` prop as an Output (`deployment-edge.ts`) — riding `artifactPath` would silently skip code deploys when a new env row lands in the same deploy (proven with tests against alchemy's real Output machinery, and re-proven live). | ||
| - **Env changes always ship** (`always-redeploy.ts`) — the deploy hook hard-links the artifact into a per-deploy-generation path so every deploy replaces the deployment, restoring the pre-existing guarantee that a rotated value reaches the running app. Cost: one deployment replacement per service per deploy, same as before this PR's base. Removed at a marked seam when upstream's `Deployment.redeployOn` (in #1061) releases. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Align deployment documentation with fingerprint-based reuse.
These statements describe the superseded always-redeploy model. The current implementation replaces deployments when the artifact or relevant environment fingerprint changes and reuses unchanged services.
.drive/projects/alchemy-provider-adoption/assets/composer-pr-body.md#L24-L24: replace the unconditional redeploy claim with the fingerprint behavior..drive/projects/alchemy-provider-adoption/assets/composer-pr-body.md#L42-L42: update the follow-up reference fromalways-redeploy.tsto the current fingerprint seam.docs/guides/deploying.md#L279-L285: state that unchanged services are reused and changed fingerprints trigger replacement.
📍 Affects 2 files
.drive/projects/alchemy-provider-adoption/assets/composer-pr-body.md#L24-L24(this comment).drive/projects/alchemy-provider-adoption/assets/composer-pr-body.md#L42-L42docs/guides/deploying.md#L279-L285
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.drive/projects/alchemy-provider-adoption/assets/composer-pr-body.md at line
24, Update .drive/projects/alchemy-provider-adoption/assets/composer-pr-body.md
lines 24 and 42 to describe the current fingerprint-based deployment seam
instead of always-redeploy.ts: unchanged services are reused, while artifact or
relevant environment fingerprint changes replace deployments. Update
docs/guides/deploying.md lines 279-285 with the same behavior, explicitly
stating that unchanged services are reused and changed fingerprints trigger
replacement.
| `destroyApp` (`ComputeLifecycle.ts:276-310`) retries any conflict 5 times with | ||
| 250ms · 2^attempt between them — 3.75 seconds of waiting in total — and it does | ||
| NOT drain the app's deployments first; it deletes the App and relies on the |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 12 'destroyApp|250|attempt|retry' node_modules/alchemy/src/Prisma/ComputeLifecycle.tsRepository: prisma/composer
Length of output: 328
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== repo files around drive/design/source =="
git ls-files | sed -n '1,120p'
echo
echo "== locate design-notes and ComputeLifecycle =="
git ls-files | rg '(\.drive/projects/alchemy-provider-adoption/design-notes\.md|ComputeLifecycle\.ts|alchemy)' | sed -n '1,200p'
echo
echo "== design note relevant text =="
if [ -f .drive/projects/alchemy-provider-adoption/design-notes.md ]; then
sed -n '160,190p' .drive/projects/alchemy-provider-adoption/design-notes.md | cat -n
fiRepository: prisma/composer
Length of output: 8518
🌐 Web query:
Alchemy destroyApp ComputeLifecycle 250ms 2^attempt retry 2.0.0-beta.67
💡 Result:
No authoritative source found that documents the exact phrase/behavior “destroyApp ComputeLifecycle 250ms 2^attempt retry 2.0.0-beta.67” in Alchemy. What the sources do show (Alchemy lifecycle / destroy): 1) Alchemy has a “destroy” phase for apps, which deletes all resources for the given stage/scope and stops execution (e.g., phase: "destroy"). [1][2] 2) Resource lifecycle handlers use this.phase with a delete phase ("delete"/handler context) and must return this.destroy() to signal deletion completion; otherwise Alchemy will not remove state. [3][4] 3) alchemy destroy is implemented as a lifecycle operation that plans every persisted resource for deletion, and deletion is idempotent via provider delete semantics. [5] 4) Destroy can be sequential (default) or parallel via destroyStrategy. [2][6] About the “250ms / 2^attempt exponential backoff” portion: - The Alchemy sources returned here do not mention “ComputeLifecycle”, “destroyApp”, “250ms”, or “2^attempt … retry” specifically. - A generic “full jitter exponential backoff” formula of the form delay = random(0, min(2^attempt * baseDelayMs, maxDelayMs)) is documented by Cloudflare Agents retries, but that is not an Alchemy-specific document. [7][8] About “2.0.0-beta.67”: - The only relevant match found is the Alchemy package version alchemy 2.0.0-beta.67 referenced as a dependency in a third-party npm package (@fllstck/nebius-alchemy), not in official Alchemy docs or changelogs tying it to “ComputeLifecycle” behavior. [9] If you can share the code snippet (especially where you see “ComputeLifecycle”, “destroyApp”, or the retry/backoff parameters) or the file/module name that logs those strings, I can re-run targeted searches specifically for that identifier and confirm whether it’s from Alchemy itself or from an embedded dependency/runtime.
Citations:
- 1: https://alchemy.run/concepts/phase/
- 2: https://alchemy-run-alchemy.mintlify.app/concepts/lifecycle
- 3: https://alchemy.run/concepts/resource
- 4: https://mintlify.wiki/alchemy-run/alchemy/concepts/lifecycle
- 5: https://alchemy.run/infrastructure-as-code/resource-lifecycle/
- 6: https://alchemy.run/concepts/scope/
- 7: https://developers.cloudflare.com/agents/api-reference/retries/
- 8: https://developers.cloudflare.com/agents/runtime/execution/retries/
- 9: https://www.npmjs.com/package/@fllstck/nebius-alchemy
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== npm metadata/lock/package references =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
if [ -f "$f" ]; then
echo "--- $f ---"
case "$f" in
package.json) jq -r '.dependencies.alchemy // .devDependencies.alchemy // .peerDependencies.alchemy // empty' "$f" ;;
*) rg -n '"alchemy"|alchemy@|node_modules/alchemy' "$f" | sed -n '1,80p' || true
esac
fi
done
echo
echo "== patch and dependency references to ComputeLifecycle/destroyApp/250ms/2^attempt/409 =="
{
git ls-files | rg '(^patches/alchemy|package.json|pnpm-lock.yaml|package-lock.json|yarn.lock|docs/design/.*|\.drive/projects/alchemy-provider-adoption/)' \
| while read -r f; do
rg -n "destroyApp|ComputeLifecycle|250|2\\^attempt|2\\*attempt|backoff|409|409" "$f" || true
done
} | sed -n '1,240p'
echo
echo "== patch file size/contents around relevant names =="
if [ -f patches/alchemy@2.0.0-beta.67.patch ]; then
wc -l patches/alchemy@2.0.0-beta.67.patch
rg -n -C 8 "destroyApp|ComputeLifecycle|250|409|backoff|attempt" patches/alchemy@2.0.0-beta.67.patch | sed -n '1,260p'
fiRepository: prisma/composer
Length of output: 4972
🌐 Web query:
site:unpkg.com alchemy@2.0.0-beta.67 ComputeLifecycle destroyApp
💡 Result:
There is no standard or widely recognized library, SDK, or official Alchemy documentation that contains a "ComputeLifecycle" class with a "destroyApp" method. The version string "2.0.0-beta.67" mentioned in your query appears in the changelogs of packages related to the AI SDK (e.g., @ai-sdk/mcp) [1][2][3], which are distinct from Alchemy's blockchain infrastructure tools. If you are working with Alchemy's Account Kit or Alchemy SDK, you may be looking for functionality related to smart contract account management or webhooks, but "ComputeLifecycle" and "destroyApp" do not correspond to methods or classes in those packages [4][5][6][7]. If this is part of a private or proprietary codebase, please consult your internal documentation or the specific repository's codebase for the definition of these identifiers.
Citations:
- 1: https://unpkg.com/@ai-sdk/mcp@2.0.22/CHANGELOG.md
- 2: https://unpkg.com/@ai-sdk/mcp@2.0.18/CHANGELOG.md
- 3: https://unpkg.com/@ai-sdk/react@4.0.50/CHANGELOG.md
- 4: https://app.unpkg.com/@alchemy/aa-core@3.19.0/files/src/index.ts
- 5: https://app.unpkg.com/alchemy-sdk@3.5.1/files/dist/es/src/api/notify-namespace.d.ts
- 6: https://app.unpkg.com/@account-kit/infra@4.84.0/files/src/client/rpcClient.ts
- 7: https://app.unpkg.com/@alchemy/aa-core@3.12.0/files/README.md
🌐 Web query:
"retry" "250 * Math" "2 ** attempt" "destroyApp"
💡 Result:
The provided query, "retry 250 * Math 2 attempt destroyApp," does not refer to a single, standard programming function or syntax. Instead, it appears to be a fragmented combination of separate technical concepts: 1. Retry Logic: Libraries such as retry [1] and p-retry [2] are commonly used in JavaScript/Node.js to handle failed operations with exponential backoff. In these contexts, you typically define a maximum number of retries and an attempt function [1][2]. 2. Project Euler Problem 250: The number "250" and "Math" frequently appear together in references to Project Euler Problem 250, which involves calculating the number of subsets of a set that satisfy certain modular arithmetic conditions [3][4][5][6][7]. Code solutions for this problem often involve loops, arrays, and modular exponentiation [3][4][7]. 3. destroyApp: This method is a standard component of the Java Micro Edition (Java ME) MIDlet lifecycle [8][9]. It is used to signal that an application should clean up resources and prepare for termination [9]. Official guidelines state that developers should not call destroyApp() directly; instead, they should use notifyDestroyed() to signal the Application Management System (AMS), which will then call destroyApp() [8][9]. There is no recognized programming construct that combines these terms as written. If you encountered this string in a specific codebase or error log, it is likely a combination of different lines of code or a misinterpretation of distinct commands.
Citations:
- 1: https://www.npmjs.com/package/retry
- 2: https://github.com/sindresorhus/p-retry
- 3: https://github.com/nayuki/Project-Euler-solutions/blob/master/java/p250.java
- 4: https://github.com/cirosantilli/project-euler-solutions/blob/master/solvers/250.py
- 5: https://github.com/roosephu/project-euler/blob/master/250.cpp
- 6: https://github.com/nayuki/Project-Euler-solutions/blob/master/mathematica/p250.mathematica
- 7: http://euler.stephan-brumme.com/250/
- 8: https://coderanch.com/t/230355/Destroying-Midlet
- 9: https://www.yumpu.com/en/document/view/47708049/iden-java-me-developer-guide-nii-developer-program
🌐 Web query:
"250ms · 2^attempt" "3.75 seconds"
💡 Result:
The values 250ms, 2^attempt, and 3.75 seconds are parameters and results associated with a specific implementation of an exponential backoff algorithm with jitter, commonly found in libraries like the Google API client for.NET [1][2]. In this configuration: - Base Delay: 250ms [1][2] - Formula: The backoff is calculated as (2^(attempt-1) * 1000ms) + jitter [1]. - Jitter: A random value is added or subtracted within the range of the delta (±250ms) [1][2]. The "3.75 seconds" figure arises in the third retry attempt (where attempt=3) [1][2]. With a base of 250ms and a multiplier of 2, the unjittered delay for the third attempt is: - Attempt 1: 2^0 * 1000ms = 1000ms (1s) [1] - Attempt 2: 2^1 * 1000ms = 2000ms (2s) [1] - Attempt 3: 2^2 * 1000ms = 4000ms (4s) [1] When applying the jitter of ±250ms to the 4s (4000ms) base for the third attempt, the range becomes 3750ms to 4250ms, or 3.75 to 4.25 seconds [1][2]. This confirms that 3.75 seconds represents the lower bound of the randomized delay for the third retry attempt in this specific implementation [1][2].
Citations:
- 1: https://github.com/gachris/OpenVision/blob/master/src/OpenVision.Api.Core/ExponentialBackOff.cs
- 2: https://googleapis.dev/dotnet/Google.Apis.Core/latest/api/Google.Apis.Util.ExponentialBackOff.html
🌐 Web query:
"2.0.0-beta.67" "destroyApp" "ComputeLifecycle"
💡 Result:
The query "2.0.0-beta.67 destroyApp ComputeLifecycle" appears to be a conflation of several different technical terms and projects. There is no single official API or function matching this exact signature. Here is the breakdown of the terms identified in your query: 1. ComputeSDK 2.0.0-beta.67: ComputeSDK is a real library for multi-provider sandbox orchestration [1][2]. Its versioning (e.g., 2.0.0-beta.67) relates to this package [3]. However, the SDK uses the method sandbox.destroy() to clean up resources, not "destroyApp" [4][5][6]. 2. destroyApp: This is a method found in other software frameworks, such as Qlik Sense, which uses DestroyApp to delete applications [7], and Actian OpenROAD, which features a DestroyApp utility [8]. 3. ComputeLifecycle: This term is typically used in analytics (e.g., calculating user lifecycle states) [9] or in framework-specific lifecycle management, such as in Android Jetpack Compose [10][11]. It is not a standard component or method name in the ComputeSDK library. In the context of ComputeSDK, the correct pattern for cleaning up resources is to use await sandbox.destroy() [4][5][1]. If you are working with a specific application or framework, please verify the exact API documentation for that system.
Citations:
- 1: https://www.computesdk.com/blog/january-2026-update/
- 2: https://github.com/computesdk/computesdk
- 3: https://github.com/gatsbyjs/gatsby/compare/gatsby@2.0.0-beta.66...gatsby@2.0.0-beta.67
- 4: https://docs.computesdk.com/reference/compute.sandbox
- 5: https://docs.computesdk.com/reference/compute
- 6: https://cdn.jsdelivr.net/npm/computesdk@2.1.1/dist/index.d.ts
- 7: https://help.qlik.com/en-US/sense-developer/May2026/Subsystems/NetSDKAPIref/Content/Qlik.Engine.Hub.DestroyApp.htm
- 8: https://docs.actian.com/openroad/6.2/Migration/Destroy_a_Single_Component_Flag_in_DestroyApp.htm
- 9: Arjun0606/smolanalytics@ef5de5f
- 10: https://developer.android.com/topic/libraries/architecture/lifecycle
- 11: https://developer.android.com/jetpack/androidx/releases/lifecycle
Align the retry-budget number with the implementation semantics.
If destroyApp waits on every retry attempt with 250ms * 2^attempt, five attempts add up to 7.75 seconds, not 3.75 seconds; otherwise count the total attempts/waits explicitly so the design note is deterministic.
🧰 Tools
🪛 LanguageTool
[grammar] ~174-~174: Ensure spelling is correct
Context: ...-310`) retries any conflict 5 times with 250ms · 2^attempt between them — 3.75 seconds...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.drive/projects/alchemy-provider-adoption/design-notes.md around lines 174 -
176, The destroyApp retry description must match its exponential-backoff
semantics: update the stated total waiting time to 7.75 seconds for five waits,
or explicitly define whether “five attempts” includes the initial call and count
the corresponding waits. Keep the surrounding deployment-draining and deletion
behavior unchanged.
| - **Compute vs App+Deployment**: adopt upstream's composite `Prisma.Compute` | ||
| (gains health-check + auto-rollback; env ordering owned internally) or the | ||
| low-level `App`+`Deployment` pair (closer to our current split; needs our own | ||
| env dependency edge)? Decide in the compute-family slice with the descriptor | ||
| rewiring in front of us. | ||
| - **State migration mechanics**: rewrite rows in place (SQL migration in the | ||
| hosted store) vs `Provider.aliases` (beta.65+ mechanism) vs | ||
| destroy-and-recreate per stage? Decide in the migration slice after testing | ||
| aliases against a scratch stage. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Close the settled adoption questions.
The implementation selected App + Deployment and on-read state migration. The design notes document both decisions. Mark these questions resolved or move the rationale to a completed-decisions section so the specification does not describe implemented work as pending.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.drive/projects/alchemy-provider-adoption/spec.md around lines 141 - 149,
The specification’s adoption questions remain marked as pending despite the
implementation decisions. Update the “Compute vs App+Deployment” entry to record
the selected App + Deployment approach and the “State migration mechanics” entry
to record on-read state migration, marking both resolved or moving their
rationale into the completed-decisions section.
| | `Prisma.Project` | Project | name | projectId | **one per Prisma Composer application**; resolved by the CLI before Alchemy runs, so no lowering yields one | | ||
| | `Prisma.Database` | Database | project, name?, region, branchId? | databaseId, connection strings | one per Module-provisioned postgres resource; never the project default; a branch-attached database is created with `branchId` and no display name (upstream refuses the combination — see [deploying.md](../../guides/deploying.md)) | | ||
| | `Prisma.Connection` | database connection info | database, name | connectionId, directConnectionString | Composer binds the DIRECT string explicitly; upstream's `databaseUrl` is pooled-first | | ||
| | `Prisma.App` | App | project, displayName, regionId, branchId? | appId, appEndpointDomain | `branchId` targets a named stage's Branch; omitted, upstream attaches the App to the project's default (production) Branch. `appEndpointDomain` is available at provision — that is what a service's own origin is read from | | ||
| | `Prisma.EnvironmentVariable` | ConfigVariable | project, class, key, value (Redacted), branchId? | environmentVariableId | production-class with no `branchId` on the default stage; preview-class with `branchId` on a named stage. Values are write-only, so upstream re-applies the desired one on every deploy | | ||
| | `Prisma.Deployment` | Deployment (ComputeVersion) + Promotion | app, artifactPath, artifactContentType, portMapping, start, promote | deploymentId, appEndpointDomain | provider reconcile: create → upload tar.gz → start → poll until running → promote; `appEndpointDomain` read **post-promote** (create-time domain is a placeholder — PRO-200). It is replaced, not updated, when its artifact fingerprint moves | |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use upstream ownership terminology in the resource inventory.
The table lists upstream alchemy/Prisma resources, but the surrounding text calls them “Our resource” and says Composer defines them. This conflicts with the upstream ownership statement at Line 8-12 and ADR-0046. Rename the column and introductory wording to distinguish lowered resources from Composer-owned resources.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/design/05-prisma-cloud/alchemy-lowering.md` around lines 86 - 91, Update
the resource inventory’s introductory wording and column heading to identify
these as upstream-owned resources lowered from Alchemy/Prisma, rather than
Composer-owned resources. Preserve the existing resource mappings and
terminology, while aligning the ownership language with the upstream ownership
statement and ADR-0046.
| ## A deployment is replaced when its environment changes | ||
|
|
||
| The platform bakes environment values into a deployment at create, and | ||
| upstream reuses a deployment whose artifact is unchanged — so a value-only | ||
| change (a rotated secret) would update the platform's variable row and never | ||
| reach the running app. Composer closes this with a deploy fingerprint | ||
| (`compute/deploy-fingerprint.ts`): the artifact hard-link directory is named | ||
| from a hash of the service's environment material, so the resolved | ||
| `artifactPath` upstream compares moves exactly when the environment does — | ||
| unchanged service, identical path, deployment reused; changed environment or | ||
| artifact, new path, replace. | ||
|
|
||
| The fingerprint hashes only non-secret material. Composer's environment rows | ||
| carry none (ADR-0042: secrets are pointers to platform variables, not | ||
| values); secret-bearing rows contribute their wiring identity, not a value. | ||
| Out-of-band rotation of a pointed platform variable is detected through its | ||
| `updatedAt` metadata, read at preflight and carried to the Alchemy process | ||
| over the framework's preflight-transport channel (a timestamp, never a | ||
| value). One accepted narrowing, recorded in the module: a value re-issued | ||
| under a stable resource identity (a connection rotated in place, a re-minted | ||
| service key) does not move the fingerprint; the deployment ships it on the | ||
| next change that does. Upstream's `Deployment.redeployOn` closes that | ||
| properly once released — alchemy resolves and diffs those inputs inside its | ||
| own encrypted state — and the fingerprint then moves onto it at a marked | ||
| seam. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Align deployment-replacement documentation with fingerprint reuse.
The documentation overstates replacement behavior. Unchanged inputs reuse the deployment, and stable-identity secret reissues are an accepted exception.
docs/design/90-decisions/ADR-0046-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.md#L104-L128: Rename the section and state that tracked artifact or environment material triggers replacement.docs/design/90-decisions/README.md#L70-L70: Replace “every deploy replaces the deployment” with the conditional replacement behavior.
🧰 Tools
🪛 LanguageTool
[grammar] ~106-~106: Ensure spelling is correct
Context: ...environment values into a deployment at create, and upstream reuses a deployment whose...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
📍 Affects 2 files
docs/design/90-decisions/ADR-0046-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.md#L104-L128(this comment)docs/design/90-decisions/README.md#L70-L70
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@docs/design/90-decisions/ADR-0046-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.md`
around lines 104 - 128, Update the ADR-0046 section title and text to describe
conditional deployment replacement: tracked artifact or environment-material
changes create a new deployment, while unchanged inputs reuse the existing
deployment and stable-identity secret reissues remain an accepted exception. In
docs/design/90-decisions/ADR-0046-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.md
lines 104-128, align the wording with the deploy fingerprint behavior. In
docs/design/90-decisions/README.md line 70, replace the claim that every deploy
replaces the deployment with this conditional behavior.
| cwd, | ||
| stage: alchemyStage, | ||
| containerEnv: containerEnv(containers), | ||
| preflightEnv: preflightEnv(preflightPayloads), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Serialize preflight payloads before writing the stack.
preflightEnv(preflightPayloads) can throw when extension IDs map to the same environment variable. Line 254 runs after writeStackFile, and the Alchemy catch converts this preflight validation failure to DEPLOY.ENGINE_FAILED.
Build the environment record after the preflight loop. Convert a collision to DEPLOY.PREFLIGHT_FAILED. Then pass the validated record to runAlchemy.
Proposed fix
const preflightPayloads = new Map<string, string>();
+ let preflightEnvironment: Record<string, string> = {};
try {
// ...
if (action === 'deploy') {
for (const extension of config.extensions) {
// ...
}
+ try {
+ preflightEnvironment = preflightEnv(preflightPayloads);
+ } catch (error) {
+ throw toStructured('DEPLOY.PREFLIGHT_FAILED', error);
+ }
}
} catch (error) {
// ...
}
- preflightEnv: preflightEnv(preflightPayloads),
+ preflightEnv: preflightEnvironment,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/0-framework/3-tooling/cli/src/operations/execute-deploy-destroy.ts`
at line 254, Update the deploy flow around the preflight loop and runAlchemy
call: build the environment record only after all preflight payloads have been
processed, catch duplicate extension-ID environment variable collisions, and
convert them to DEPLOY.PREFLIGHT_FAILED. Pass the validated record to runAlchemy
instead of invoking preflightEnv inline during stack writing.
| function artifactSha256(artifactPath: string): string { | ||
| const stat = fs.statSync(artifactPath); | ||
| const identity = `${artifactPath}:${String(stat.size)}:${String(stat.mtimeMs)}`; | ||
| const memoized = artifactHashes.get(identity); | ||
| if (memoized !== undefined) return memoized; | ||
| const hash = crypto.createHash('sha256'); | ||
| const fd = fs.openSync(artifactPath, 'r'); | ||
| try { | ||
| const buffer = Buffer.allocUnsafe(1024 * 1024); | ||
| let read = fs.readSync(fd, buffer, 0, buffer.length, null); | ||
| while (read > 0) { | ||
| hash.update(buffer.subarray(0, read)); | ||
| read = fs.readSync(fd, buffer, 0, buffer.length, null); | ||
| } | ||
| } finally { | ||
| fs.closeSync(fd); | ||
| } | ||
| const digest = hash.digest('hex'); | ||
| artifactHashes.set(identity, digest); | ||
| return digest; | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
Hash the artifact without blocking the event loop.
artifactSha256 runs inside an async provider body, but it uses fs.statSync, fs.openSync, and fs.readSync. The comment states artifacts reach hundreds of megabytes. The synchronous read loop blocks the event loop for the whole file. Other providers in the same converge and the emulator HTTP client cannot make progress during that time. Use the async file APIs so only this provider waits.
♻️ Proposed async hashing
-const artifactHashes = new Map<string, string>();
+const artifactHashes = new Map<string, string>();
-function artifactSha256(artifactPath: string): string {
- const stat = fs.statSync(artifactPath);
+async function artifactSha256(artifactPath: string): Promise<string> {
+ const stat = await fs.promises.stat(artifactPath);
const identity = `${artifactPath}:${String(stat.size)}:${String(stat.mtimeMs)}`;
const memoized = artifactHashes.get(identity);
if (memoized !== undefined) return memoized;
const hash = crypto.createHash('sha256');
- const fd = fs.openSync(artifactPath, 'r');
- try {
- const buffer = Buffer.allocUnsafe(1024 * 1024);
- let read = fs.readSync(fd, buffer, 0, buffer.length, null);
- while (read > 0) {
- hash.update(buffer.subarray(0, read));
- read = fs.readSync(fd, buffer, 0, buffer.length, null);
- }
- } finally {
- fs.closeSync(fd);
- }
+ for await (const chunk of fs.createReadStream(artifactPath)) {
+ hash.update(chunk as Buffer);
+ }
const digest = hash.digest('hex');
artifactHashes.set(identity, digest);
return digest;
}The call site then becomes const artifactHash = await artifactSha256(news.artifactPath);.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function artifactSha256(artifactPath: string): string { | |
| const stat = fs.statSync(artifactPath); | |
| const identity = `${artifactPath}:${String(stat.size)}:${String(stat.mtimeMs)}`; | |
| const memoized = artifactHashes.get(identity); | |
| if (memoized !== undefined) return memoized; | |
| const hash = crypto.createHash('sha256'); | |
| const fd = fs.openSync(artifactPath, 'r'); | |
| try { | |
| const buffer = Buffer.allocUnsafe(1024 * 1024); | |
| let read = fs.readSync(fd, buffer, 0, buffer.length, null); | |
| while (read > 0) { | |
| hash.update(buffer.subarray(0, read)); | |
| read = fs.readSync(fd, buffer, 0, buffer.length, null); | |
| } | |
| } finally { | |
| fs.closeSync(fd); | |
| } | |
| const digest = hash.digest('hex'); | |
| artifactHashes.set(identity, digest); | |
| return digest; | |
| } | |
| async function artifactSha256(artifactPath: string): Promise<string> { | |
| const stat = await fs.promises.stat(artifactPath); | |
| const identity = `${artifactPath}:${String(stat.size)}:${String(stat.mtimeMs)}`; | |
| const memoized = artifactHashes.get(identity); | |
| if (memoized !== undefined) return memoized; | |
| const hash = crypto.createHash('sha256'); | |
| for await (const chunk of fs.createReadStream(artifactPath)) { | |
| hash.update(chunk as Buffer); | |
| } | |
| const digest = hash.digest('hex'); | |
| artifactHashes.set(identity, digest); | |
| return digest; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/1-prisma-cloud/0-lowering/local-target/src/compute.ts` around lines
41 - 61, Update artifactSha256 to be asynchronous and replace
fs.statSync/openSync/readSync with promise-based file APIs, preserving the
existing SHA-256 digest and memoization behavior. Update its provider call site
to await artifactSha256 when computing artifactHash, ensuring hashing yields to
the event loop while retaining proper file-handle cleanup.
| if (!fs.existsSync(linked)) { | ||
| try { | ||
| fs.linkSync(artifactPath, linked); | ||
| } catch { | ||
| // A filesystem without hard links still gets the fingerprinted path. | ||
| fs.copyFileSync(artifactPath, linked); | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
The copy fallback can expose a half-written artifact.
fs.existsSync and fs.linkSync are not atomic together. If a concurrent run creates the link between the two calls, linkSync throws EEXIST, the catch runs copyFileSync, and copyFileSync truncates and rewrites the file in place. A reader that opens linked during that window reads a partial artifact.
packageComputeArtifact already writes through a temp file and renames for the same reason (artifact.ts lines 197-202). Apply the same pattern here, and treat EEXIST as success.
🔒️ Proposed fix
const linked = path.join(dir, path.basename(artifactPath));
if (!fs.existsSync(linked)) {
try {
fs.linkSync(artifactPath, linked);
} catch (error) {
- // A filesystem without hard links still gets the fingerprinted path.
- fs.copyFileSync(artifactPath, linked);
+ // A concurrent run already linked the same bytes — nothing to do.
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
+ // A filesystem without hard links still gets the fingerprinted path.
+ // Copy-then-rename so a concurrent reader never sees a partial file.
+ const tmp = path.join(dir, `.${path.basename(artifactPath)}.${crypto.randomUUID()}.tmp`);
+ fs.copyFileSync(artifactPath, tmp);
+ fs.renameSync(tmp, linked);
+ }
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!fs.existsSync(linked)) { | |
| try { | |
| fs.linkSync(artifactPath, linked); | |
| } catch { | |
| // A filesystem without hard links still gets the fingerprinted path. | |
| fs.copyFileSync(artifactPath, linked); | |
| } | |
| } | |
| const linked = path.join(dir, path.basename(artifactPath)); | |
| if (!fs.existsSync(linked)) { | |
| try { | |
| fs.linkSync(artifactPath, linked); | |
| } catch (error) { | |
| // A concurrent run already linked the same bytes — nothing to do. | |
| if ((error as NodeJS.ErrnoException).code !== 'EEXIST') { | |
| // A filesystem without hard links still gets the fingerprinted path. | |
| // Copy-then-rename so a concurrent reader never sees a partial file. | |
| const tmp = path.join(dir, `.${path.basename(artifactPath)}.${crypto.randomUUID()}.tmp`); | |
| fs.copyFileSync(artifactPath, tmp); | |
| fs.renameSync(tmp, linked); | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/1-prisma-cloud/0-lowering/lowering/src/compute/deploy-fingerprint.ts`
around lines 100 - 107, Update the artifact linking fallback around the
existsSync/linkSync flow to avoid writing directly to linked: copy artifactPath
to a uniquely named temporary path in the same directory, then atomically rename
it to linked. Handle an EEXIST from either the hard-link or final rename
operation as success, while preserving fallback behavior for other filesystem
errors.
| describe('state round-trip of legacy rows through the hosted state layer', () => { | ||
| // The REAL layer (stateLayerAgainst → stock HTTP client → on-read | ||
| // migration) against an in-process fake of the platform state API — the | ||
| // same wiring a deploy uses, so this proves the layer applies the | ||
| // migration, not just that the pure function works. | ||
| const fake = new FakeStateApi(); | ||
| const stack = 'legacy-state-stack'; | ||
| const stage = 'br_legacy'; | ||
|
|
||
| beforeAll(async () => { | ||
| await fake.start(); | ||
| }); | ||
|
|
||
| afterAll(async () => { | ||
| await fake.stop(); | ||
| }); | ||
|
|
||
| const stackContext = Layer.succeed(Stack, { | ||
| name: stack, | ||
| stage, | ||
| resources: {}, | ||
| bindings: {}, | ||
| actions: {}, | ||
| }); | ||
|
|
||
| const runLayer = <A>(use: (service: StateService) => Effect.Effect<A, unknown>): Promise<A> => { | ||
| const layer = stateLayerAgainst(fake.origin, { | ||
| projectId: 'proj-legacy', | ||
| branchId: 'br-legacy', | ||
| }).pipe(Layer.provide(stackContext)) as unknown as Layer.Layer<State>; | ||
| return Effect.runPromise( | ||
| Effect.gen(function* () { | ||
| const service = yield* yield* State; | ||
| return yield* use(service).pipe(Effect.orDie); | ||
| }).pipe(Effect.provide(layer)) as Effect.Effect<A>, | ||
| ); | ||
| }; | ||
|
|
||
| test('an old-shape Database row persisted as-is is read back in the upstream shape', async () => { | ||
| const row = (await runLayer((service) => | ||
| Effect.gen(function* () { | ||
| yield* service.set({ stack, stage, fqn: 'data-db', value: legacyDatabaseRow() }); | ||
| return yield* service.get({ stack, stage, fqn: 'data-db' }); | ||
| }), | ||
| )) as MigratedRow; | ||
| expect(row.resourceType).toBe('Prisma.Database'); | ||
| expect(row.attr).toMatchObject({ databaseId: 'db-1', databaseName: 'data' }); | ||
| expect(row.props).toEqual({ project: 'proj-1', name: 'data', region: 'us-east-1' }); | ||
| }); | ||
|
|
||
| test('an old-shape Connection row round-trips with the Redacted secret intact', async () => { | ||
| const row = (await runLayer((service) => | ||
| Effect.gen(function* () { | ||
| yield* service.set({ stack, stage, fqn: 'data-conn', value: legacyConnectionRow() }); | ||
| return yield* service.get({ stack, stage, fqn: 'data-conn' }); | ||
| }), | ||
| )) as MigratedRow; | ||
| expect(row.resourceType).toBe('Prisma.Connection'); | ||
| expect(row.attr).toMatchObject({ connectionId: 'conn-1', databaseId: 'db-1' }); | ||
| const direct = row.attr['directConnectionString']; | ||
| expect(Redacted.isRedacted(direct)).toBe(true); | ||
| expect(Redacted.value(direct as Redacted.Redacted<string>)).toBe(DIRECT_URL); | ||
| // The databaseUrl mirror keeps the value usable where the conventional | ||
| // application URL is read. | ||
| expect(Redacted.value(row.attr['databaseUrl'] as Redacted.Redacted<string>)).toBe(DIRECT_URL); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
Cover getReplacedResources in the round-trip suite.
The round-trip suite proves the layer migrates rows returned by get. migrateRowsOnRead in state/layer.ts also wraps getReplacedResources, and that wrapper has no test. A replaced legacy row read back through the layer is the exact path the engine uses to plan a delete of the displaced generation. Add one test that seeds a replaced legacy row and reads it back through runLayer.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/1-prisma-cloud/0-lowering/lowering/src/state/__tests__/legacy-resources.test.ts`
around lines 787 - 853, The round-trip suite currently covers only get; add a
test using runLayer that seeds a replaced legacy row via the service API and
reads it through getReplacedResources. Assert the returned row is migrated to
the upstream shape, including relevant resource attributes and preserved
Redacted values, so the wrapped replacement-read path is exercised.
| return { | ||
| key: inputKey(address), | ||
| value: JSON.stringify(document), | ||
| absent, | ||
| generated, | ||
| secrets: [...sentinels.values()], | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
Deduplicate and sort the collected secret names.
sentinels is keyed by sentinel box identity, not by platform variable name. Two leaves that bind the same variable produce two entries with the same value. The iteration order follows the binding traversal order. The deploy hook folds these names into the environment fingerprint. A binding refactor that only reorders or duplicates leaves therefore changes the fingerprint and replaces the deployment, with no change to the secret set. Emit a sorted, unique list so the fingerprint depends only on the set of referenced variables.
♻️ Proposed change
return {
key: inputKey(address),
value: JSON.stringify(document),
absent,
generated,
- secrets: [...sentinels.values()],
+ secrets: [...new Set(sentinels.values())].sort(),
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return { | |
| key: inputKey(address), | |
| value: JSON.stringify(document), | |
| absent, | |
| generated, | |
| secrets: [...sentinels.values()], | |
| }; | |
| return { | |
| key: inputKey(address), | |
| value: JSON.stringify(document), | |
| absent, | |
| generated, | |
| secrets: [...new Set(sentinels.values())].sort(), | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/1-prisma-cloud/1-extensions/target/src/serializer.ts` around lines
518 - 524, Update the serializer return in the surrounding serialization
function to deduplicate the values from sentinels and sort them
deterministically before assigning secrets. Preserve the existing secret names
while ensuring duplicate bindings produce one entry and traversal order cannot
affect the result.
Composer's six hand-written Alchemy resources for Prisma Cloud are gone. Deploys and local dev now run on the official
alchemy/Prismaprovider (alchemy 2.0.0-beta.67):Why: upstream tracks the Management API so we don't, and its deploy lifecycle is better than ours was — cleanup of failed deployments, terminal-status fast-fail, post-promote endpoint observation. Decision record: ADR-0043.
What changed
PrismaComposer.*; the old ids are aliases so existing state rows resolve.Project/Database/Connectionclasses, driven by our own auth layer (PrismaEnvironmentfromPRISMA_SERVICE_TOKEN, no interactive profile store; one base-URL resolver shared with our SDK client). Branch stages create their database attached with a generated physical name (upstream correctly refuses explicit-name-plus-branch; verified against PDP source).directConnectionStringis bound explicitly — upstream'sdatabaseUrlis pooled-first.App/Deployment/EnvironmentVariable, not compositeCompute: theCOMPOSER_*_ORIGINself-edge needs the App to exist before env rows, andDeploymenthas no build path at all (ADR-0005 by structure). The env→deployment ordering edge rides the deployment'sappprop as an Output (deployment-edge.ts) — ridingartifactPathwould silently skip code deploys when a new env row lands in the same deploy (proven with tests against alchemy's real Output machinery, and re-proven live).compute/deploy-fingerprint.ts) — the artifact hard-link directory is named from a hash of the service's environment material, so upstream reuses the deployment when nothing changed and replaces it when the env or artifact did. The hashed material is non-secret by construction (ADR-0042 rows carry literals and pointers, never values); out-of-band rotation of a pointed platform variable is detected via itsupdatedAtmetadata, read at preflight and carried across the CLI→Alchemy process boundary on a new preflight-transport channel. Swaps onto upstream'sDeployment.redeployOn(in #1061) at a marked seam when it releases.state/legacy-resources.ts) — old type-ids and attribute shapes rewrite in the hosted store; the retired poisonDATABASE_URLrows are reportedretained(state row dropped, platform variable untouched). Fresh projects get the poison back by a different route:application.provisionclaimsDATABASE_URL/DATABASE_URL_POOLEDwith"-"via create-only writes (never tracked as resources, never modified or deleted), because the platform otherwise self-heals a missingDATABASE_URLon first deploy with a live credential to one of the app's own databases. Existing rows — platform-seeded or ours — 409 and no-op. The authoring-side name ban remains.Verified
Operator notes (also in docs/guides/deploying.md)
"-"placeholder inDATABASE_URL/DATABASE_URL_POOLEDis deliberate and self-restoring; deleting it by hand is not useful (the next deploy's claim or the platform's template filler recreates the row). A user-set value wins over both — the guide has the details.Follow-up (tracked in TML-3156)
When upstream PR alchemy-run/alchemy#1061 merges and releases: bump alchemy, delete our bucket resources (upstream now ships them with capability bindings), drop the alchemy pnpm patch, and move the deploy fingerprint onto
Deployment.redeployOnat the marked seam.🤖 Generated with Claude Code