Skip to content

Stored auth profile silently overrides CLOUDFLARE_ACCOUNT_ID, deploying to the wrong account with a success message #1083

Description

@artile

Version

alchemy@2.0.0-beta.67, Node 22.22.3, macOS.

Summary

A stored auth profile silently outranks CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN. The deploy goes to the profile's account, reports Done: 4 succeeded, and prints a url for that account — with no hint that the environment asked for a different one. CI=1 alone does not override it, even though the CLI's own error message elsewhere advertises CI=1 as the way to "use environment-variable credentials".

The result is a cross-account misdeploy that looks like a clean success.

Setup

~/.alchemy/profiles.json:

{ "version": 0, "profiles": { "default": { "Cloudflare": {
  "method": "oauth", "accountId": "<ACCOUNT_A>", "scopes": ["workers:write", "..."] } } } }

.env — a token scoped to a different account, which has no access to ACCOUNT_A:

CLOUDFLARE_API_TOKEN=<token valid only for ACCOUNT_B>
CLOUDFLARE_ACCOUNT_ID=<ACCOUNT_B>

Stack is a Cloudflare.Website.StaticSite with an explicit name.

Steps

export $(grep -E '^CLOUDFLARE_' .env | xargs)
rm -rf .alchemy/state/<stack>/dev          # force a create, not an update
npx alchemy deploy --stage dev --yes ./alchemy.local.run.ts

Actual

[Worker] created
Done: 4 succeeded
{ url: 'https://landing.<ACCOUNT_A-subdomain>.workers.dev' }

The Worker is created in ACCOUNT_A. Verified directly against the API:

GET /accounts/<ACCOUNT_B>/workers/scripts  ->  []
GET /accounts/<ACCOUNT_A>/workers/scripts  ->  [..., "landing", ...]

Prefixing CI=1 changes nothing — still ACCOUNT_A.

Expected

One of:

  1. explicit CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN take precedence over a stored profile; or
  2. the deploy fails with a conflict error naming both accounts; or
  3. at minimum, a warning, and a url output that reflects where the Worker actually went.

Silently preferring the profile is the one behaviour that cannot be caught by reading the output.

Workaround

Passing a profile name that has no stored credentials, together with CI=1:

CI=1 npx alchemy deploy --stage dev --yes --profile <unused-name> ./alchemy.local.run.ts

Either flag alone is not enough. After alchemy profile clear --profile default, the unused name gets persisted as {"method":"env"} and --profile <name> alone then works without CI=1.

Two smaller things noticed alongside:

  • ALCHEMY_PROFILE is only read from the real environment, not from .env, while CLOUDFLARE_* are read from .env. Understandable given ordering, but surprising and undocumented.
  • The url output stayed wrong on every misdirected run, naming the old subdomain rather than the account actually deployed to. Worth deriving it from the deploy result instead of from resolved config.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions