Skip to content

fix(core): persist resolved bindings on terminal commits#266

Open
sam-goodwin wants to merge 1 commit into
mainfrom
sam/fix-drizzle-worker-redundant-deploy
Open

fix(core): persist resolved bindings on terminal commits#266
sam-goodwin wants to merge 1 commit into
mainfrom
sam/fix-drizzle-worker-redundant-deploy

Conversation

@sam-goodwin
Copy link
Copy Markdown
Contributor

After reconcile runs we know each binding's fully-resolved data payload (bindingOutputs) — the same payload the provider just shipped. The terminal created / updated / replaced commits were instead persisting raw node.bindings, which still hold unresolved Output expressions for any cross-resource reference (e.g. a Hyperdrive binding carrying id: hyperdrive.hyperdriveId).

JSON-serializing an Output silently drops the value, so the state file stores { name, /* id missing */ }. On the next plan the binding diff fires (id: undefinedid: <resolved>) and forces a spurious update.

Reproduces in examples/cloudflare-neon-drizzle:

bun alchemy destroy --yes
bun alchemy deploy --yes  # 5 created
bun alchemy deploy --yes  # before: Api updated; after: 5 noop
  yield* commit<CreatedResourceState>({
    status: "created",
    ...
-   bindings: excludeDeletedBindings(node.bindings),
+   bindings: bindingOutputs,
  });

Same change in the updated and replaced terminal commits. Intermediate creating / updating / replacing commits keep raw node.bindings since they may run before upstreams have published their attrs.

Regression test in packages/alchemy/test/apply.test.ts deploys a stack twice where one resource's binding references another's output, and asserts the second deploy noops both — and that the persisted binding payload has the peer value resolved.

After `reconcile` runs, we know each binding's fully-resolved data
payload (`bindingOutputs`) — the same payload the provider just applied
to the cloud. The `created`/`updated`/`replaced` commits were instead
persisting the raw `node.bindings`, which still contain unresolved
Output expressions for any cross-resource reference (e.g. a Hyperdrive
binding carrying `id: hyperdrive.hyperdriveId`).

JSON-serializing an Output expression silently drops the value, so the
state file stores `{ name, /* id missing */ }`. On the next plan, the
binding diff fires (`id: undefined` ↦ `id: <resolved>`) and forces a
spurious update — visible in the cloudflare-neon-drizzle example as the
Worker re-deploying on every second `alchemy deploy`.

Persist `bindingOutputs` in the terminal commits so what we store
matches what we shipped. Intermediate `creating`/`updating`/`replacing`
commits keep using raw `node.bindings` since they may run before
upstreams have published their attrs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alchemy-version-bot
Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

bun add alchemy@https://pkg.ing/alchemy/794b6e0

@alchemy.run/better-auth

bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/794b6e0

@alchemy.run/pr-package

bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/794b6e0

@alchemy-version-bot
Copy link
Copy Markdown
Contributor

alchemy-version-bot Bot commented May 7, 2026

Website Preview Deployed

URL: https://alchemyeffectwebsite-worker-pr-266-do2grwbshjo6h7uy.testing-2b2.workers.dev

Built from commit 794b6e0.


This comment updates automatically with each push.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant