Skip to content

[wrangler] add replace mode for --secrets-file - #15357

Closed
theoephraim wants to merge 7 commits into
cloudflare:mainfrom
theoephraim:secrets-file-replace-mode
Closed

[wrangler] add replace mode for --secrets-file#15357
theoephraim wants to merge 7 commits into
cloudflare:mainfrom
theoephraim:secrets-file-replace-mode

Conversation

@theoephraim

@theoephraim theoephraim commented Aug 25, 2026

Copy link
Copy Markdown

Builds on #15256; the base --secrets-file/--var support for wrangler preview is reviewed there. This PR's own changes are the last two commits.

Fixes #15343

NOTE - in draft. still working through some of the details around how this feature would interact with new previews and inheriting env from base config.

This PR adds an opt-in "replace mode" for secrets when deploying with --secrets-file, so that remote secrets not present in the provided file can be deleted instead of always being preserved. It also extends the mode to the private beta wrangler preview surface: wrangler preview gains --secrets-file-mode on top of the --secrets-file support added in #15256, and wrangler preview secret bulk gains --secrets-file-mode (see "Preview command coverage" below).

The problem

--secrets-file is forcibly additive today: wrangler deploy sets keepSecrets whenever a secrets file is supplied, and wrangler versions upload sets it unconditionally, so the upload always sends keep_bindings: ["secret_text", "secret_key"]. There is no way to converge the remote secret set to a declared set; secrets removed from the file linger on the Worker forever.

The new flag

--secrets-file-mode <merge|replace> on wrangler deploy and wrangler versions upload (and, with preview-specific semantics described below, on wrangler preview and wrangler preview secret bulk):

  • merge (the default, and the behavior when the flag is omitted) keeps remote secrets that are not present in the file. Existing behavior is completely unchanged.
  • replace drops them: Wrangler simply does not send the secret_text/secret_key entries in keep_bindings, and the Workers upload API's default behavior (no keep_bindings, no inherit bindings) is full replacement. Replace mode is purely client-side.
wrangler deploy --secrets-file .env.production --secrets-file-mode replace

Flag design notes:

  • A mode enum was chosen over a boolean (e.g. --replace-secrets) because it names the existing default (merge), leaves room for future modes, and matches existing enum-valued flags such as --containers-rollout <immediate|gradual|none>, using the same choices mechanism.
  • A --keep-secrets boolean mirroring --keep-vars was also considered and rejected, for two reasons. First, the defaults would be opposite: --keep-vars effectively defaults to false (vars are replaced by the config on every deploy), while --keep-secrets would have to default to true, so the symmetric naming would suggest symmetric behavior that does not exist. Second, the symmetry is shallower than it looks at the API level: the upload API only replaces the secret set when the upload actually carries secret bindings, so --keep-secrets=false without --secrets-file could not be honored at all (there is nothing in the upload to replace with) and would either silently do nothing or need the same "requires --secrets-file" guard this flag has, at which point it is a mode of the secrets file rather than an independent switch. Tying the name to --secrets-file makes that dependency explicit. There is also an existing entanglement where --keep-vars implies keeping secrets; a new independent --keep-secrets would have had to define conflict semantics against that side effect, whereas a file-scoped mode simply takes precedence over it (see Semantics below).
  • The flag lives in sharedDeployVersionsArgs next to --secrets-file and --keep-vars, so it is shared by deploy and versions upload like the flag it modifies.
  • Passing --secrets-file-mode without --secrets-file is an error (raised in validateWorkerProps(), covering both commands).

Semantics

  • Secrets declared in the secrets.required config field are always kept: they still get explicit { type: "inherit" } bindings via addRequiredSecretsInheritBindings() and survive the upload. Replace mode only drops secrets that are neither supplied in the file nor declared as required, so a config-declared secret set stays coherent with the file-supplied one.
  • Before uploading in replace mode, Wrangler fetches the remote secret names and always logs a warning listing the ones that will be dropped (the same in interactive and CI sessions, so CI logs carry the audit trail), but does not prompt for confirmation. No confirmation because the flag is an explicit opt-in whose documented purpose is removing unlisted secrets, and the analogous vars replacement (the default --keep-vars=false behavior) deletes remote vars without prompting. Under --strict the upload aborts instead of continuing, consistent with how the other pre-upload conflict checks in preUploadApiChecks() treat strict mode (conflicts abort instead of auto-continuing).
  • Replace mode wins over the keep-secrets side effect of --keep-vars (which historically also kept secrets). --keep-vars still keeps vars; only the secret handling follows the explicitly requested mode.
  • For wrangler versions upload, replace mode means the new version carries only the supplied secrets plus the secrets.required inherit bindings. Gradual deployments implication: the removal only takes effect for traffic served by the new version, and old versions still have the old secrets while they receive a share of traffic. The pre-upload warning wording reflects this ("will be removed when this version is deployed").
  • The unsafe bindings path and Pages are untouched (the Pages-to-Workers delegation passes secretsFileMode: undefined).

Preview command coverage

The mode flag now covers the private beta wrangler preview surface too, in two places. The base feature it modifies (wrangler preview --secrets-file, which attaches the file's secrets to the new Preview deployment as secret_text bindings, plus --var) is implemented and reviewed in #15256; this PR only adds the mode on top of that plumbing.

wrangler preview --secrets-file <file> --secrets-file-mode replace converges the new Preview deployment's secret set to the file. Mechanism: create-then-patch, not create-time exclusion. A Preview deployment's effective secret set is composed server-side; on top of the request env, the server can merge in secrets from the Preview base config on the parent Worker and secrets carried over from the Preview's earlier deployments (e.g. ones added with wrangler preview secrets put). The deployment creation API has no keep_bindings-style knob to suppress that merge, and merge-patch null deletions are only defined for the PATCH endpoint, so the create request cannot express "and nothing else". Instead, replace mode reads the effective env off the deployment creation response (falling back to fetching the deployment if the API omits the field), computes the secret_text bindings that are neither in the file nor in secrets.required, and if that set is non-empty logs the same always-on warning and cuts one follow-up deployment patching those bindings to null. This is robust to whatever the server-side merge does: wrangler converges the set it can actually observe rather than predicting the merge client-side. The tradeoff is a second Preview deployment when something is dropped (none when the set already converges); for an ephemeral preview that history costs nothing, and the follow-up deployment keeps the original message/tag annotations because the patch deliberately does not touch them.

wrangler preview secret bulk <file> --secrets-file-mode replace converges an existing Preview's secret set to the file. Here no create is involved, so it is a single patch: wrangler fetches the latest Preview deployment's env, computes the secret_text bindings that are neither among the file's keys nor in secrets.required, warns, and includes null deletions for them in the same merge-patch that uploads the file's secrets, producing one new Preview deployment. Keys the file explicitly sets to null are already deletions in the existing additive semantics and are excluded from the replace-computed set, so they are neither deleted nor reported twice.

Preview-specific flag design and semantics notes:

  • Flag naming for preview secret bulk: the file is a positional argument rather than --secrets-file, so a plain --mode <merge|replace> was considered. --secrets-file-mode was chosen anyway, for uniformity: it is one flag name with one meaning across deploy, versions upload, preview, and preview secret bulk, so knowledge transfers between surfaces and scripts can pass the same flag everywhere. A bare --mode is also ambiguous on a command that could plausibly grow other modes, while --secrets-file-mode still reads correctly here because the positional argument is a secrets file (the same JSON/.env format).
  • secrets.required applies to previews the same way: replace mode never drops a secret named there. Previews have no inherit-binding mechanism, so nothing needs to be added for required secrets; they are simply excluded from the deletion set. This keeps a config-declared secret contract consistent across production deploys and previews.
  • Only secret_text bindings count as secrets on the preview surface, matching the existing preview secret list/put/bulk commands, which exclusively read and write secret_text (the deploy path also handles secret_key, which does not appear on Preview deployments).
  • No --strict abort on the preview surface: wrangler preview and preview secret bulk have no --strict flag today, so replace mode there is warn-only, consistent with the rest of the (beta) preview UX. If previews grow a --strict flag, wiring the abort in is a one-liner next to the warning.
  • --secrets-file-mode without --secrets-file errors on wrangler preview (via validateArgs, before the Worker is built), with the same message as deploy. There is deliberately no such error on preview secret bulk: its secrets input is the positional file or piped stdin, so the mode always has an input to apply to, and the existing "no content found" early-exit already fires before any API call when there is none.
  • Without the new flags, both preview commands behave exactly as before (pinned by tests).

Implementation

  • packages/wrangler/src/deployment-bundle/deploy-args.ts: new secrets-file-mode arg (choices merge/replace), updated --secrets-file help text.
  • packages/wrangler/src/deployment-bundle/merge-config-args.ts, packages/deploy-helpers/src/shared/types.ts: thread secretsFileMode into the shared deploy/versions props.
  • packages/deploy-helpers/src/deploy/deploy.ts and versions-upload.ts: compute keepSecrets from the mode.
  • packages/deploy-helpers/src/deploy/helpers/check-remote-secrets-override.ts: new getSecretsDroppedByReplaceMode() helper (reuses the existing remote secrets fetch, tolerates missing Workers).
  • packages/deploy-helpers/src/deploy/helpers/validate-worker-props.ts: flag combination validation plus the pre-upload deletion warning (and the strict-mode abort).
  • packages/deploy-helpers/src/preview/preview.ts: PreviewArgs gains secretsFileMode (on top of [wrangler] Add --secrets-file and --var flags to wrangler preview #15256's secretsFile parsing and create-time attachment), and convergePreviewDeploymentSecrets() implements the replace-mode follow-up patch. The secret-value redaction from [wrangler] Add --secrets-file and --var flags to wrangler preview #15256 now runs after the convergence, so the follow-up deployment's echoed env is redacted too.
  • packages/wrangler/src/preview/preview.ts: the new --secrets-file-mode flag plus the mode-without-file validateArgs check.
  • packages/wrangler/src/preview/secrets/index.ts: new fetchPreviewDeploymentSecretNames() helper (reads the latest deployment's secret_text binding names, mapping the API's no-deployment/not-found errors to the existing user-facing messages).
  • packages/wrangler/src/preview/secrets/bulk.ts: the --secrets-file-mode flag and the replace-mode deletion merge into the single patch.

Tests cover: replace mode sends no secret keep_bindings (deploy and versions upload), explicit merge and default behavior unchanged, the deletion warning lists only dropped secrets and excludes secrets.required, the warning is emitted in both interactive and non-interactive sessions and the upload proceeds without prompting, --strict aborts the upload when secrets would be dropped, secrets.required inherit bindings survive replace mode, the flag without --secrets-file errors, invalid mode values are rejected, and unit tests for getSecretsDroppedByReplaceMode().

On the preview surface (the base --secrets-file attachment behavior is tested in #15256), tests additionally cover: merge/default cutting no follow-up deployment, replace deleting exactly the unlisted secret_text bindings (plain bindings, file-supplied secrets, and secrets.required untouched) with the follow-up deployment reported to the user, the fallback fetch when the create response omits env, secret values echoed back on the follow-up deployment being redacted from --json output, mode-without-file and invalid-mode errors on wrangler preview, preview secret bulk replace deletions in a single patch (including no double-deletion of explicitly nulled keys and correct created/deleted counts), merge mode never reading the current deployment, and replace-mode error mapping when the Preview or its deployments do not exist.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: the flag is fully described in wrangler deploy --help and the changeset; a follow-up cloudflare-docs PR will document --secrets-file-mode alongside the existing --secrets-file docs once this is merged and released.

A picture of a cute animal (not mandatory, but encouraged)

A bad file path or malformed contents now fails before the preview
resource is created or assets are uploaded, instead of leaving a
half-finished preview behind.
The API may echo the uploaded env on the deployment response; strip
secret_text values as soon as it is received so --json output can never
print them, matching how preview secret list only outputs names/types.
The diverged-config warning compared top-level bindings against config
preview bindings only, so names supplied via --var or --secrets-file
were reported as missing even though they were uploaded. Compare against
the env that was actually sent with the deployment instead.
@changeset-bot

changeset-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 229e5af

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@cloudflare/deploy-helpers Minor
wrangler Minor
@cloudflare/remote-bindings Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Aug 25, 2026
@workers-devprod
workers-devprod requested review from a team and emily-shen and removed request for a team August 25, 2026 18:09
@workers-devprod

workers-devprod commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/preview-secrets-file-flag.md: [@cloudflare/wrangler]
  • .changeset/secrets-file-replace-mode.md: [@cloudflare/wrangler]
  • packages/deploy-helpers/src/deploy/deploy.ts: [@cloudflare/wrangler]
  • packages/deploy-helpers/src/deploy/helpers/check-remote-secrets-override.ts: [@cloudflare/wrangler]
  • packages/deploy-helpers/src/deploy/helpers/validate-worker-props.ts: [@cloudflare/wrangler]
  • packages/deploy-helpers/src/deploy/versions-upload.ts: [@cloudflare/wrangler]
  • packages/deploy-helpers/src/preview/preview.ts: [@cloudflare/wrangler]
  • packages/deploy-helpers/src/shared/types.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/deploy/check-remote-secrets-override.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/deploy/secrets.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/preview.secret.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/preview.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/versions/secrets.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/deployment-bundle/deploy-args.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/deployment-bundle/merge-config-args.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/pages/run-workers-deploy.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/preview/preview.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/preview/secrets/bulk.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/preview/secrets/index.ts: [@cloudflare/wrangler]

@pkg-pr-new

pkg-pr-new Bot commented Aug 25, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15357

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15357

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@15357

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15357

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15357

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15357

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15357

miniflare

npm i https://pkg.pr.new/miniflare@15357

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15357

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15357

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15357

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15357

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@15357

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15357

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15357

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15357

wrangler

npm i https://pkg.pr.new/wrangler@15357

commit: 229e5af

theoephraim and others added 3 commits August 25, 2026 11:21
Add an opt-in --secrets-file-mode <merge|replace> flag to wrangler deploy
and wrangler versions upload. The default (merge) keeps the existing
additive behavior. Replace mode stops sending the secret_text/secret_key
keep_bindings entries so remote secrets that are neither supplied in the
secrets file nor declared in secrets.required are deleted (deploy) or not
carried into the new version (versions upload). Wrangler always warns
which remote secrets will be dropped (no confirmation prompt), and aborts
under --strict. The flag errors when used without --secrets-file.
Extend --secrets-file-mode to the private beta preview surface, on top of
the base --secrets-file/--var support for wrangler preview added in the
preview-secrets-file-var-flags branch (PR cloudflare#15256). With
--secrets-file-mode replace, wrangler preview converges the new Preview
deployment's secret set to the file by reading the effective env off the
create response and cutting one follow-up deployment that deletes the
unlisted secrets (the server-side merge of base config and carried-over
secrets cannot be suppressed in the create request); the secret-value
redaction now runs after that convergence so the follow-up deployment's
echoed env is redacted too. wrangler preview secret bulk accepts
--secrets-file-mode replace as well, deleting existing secrets not
present in the input within the same patch. secrets.required names are
never dropped, the deletion warning is always logged, and behavior
without the new flag is unchanged.
@theoephraim

Copy link
Copy Markdown
Author

Closing this for now in favor of the design discussion in #15343. The client-side implementation here (replace mode for deploy and versions upload, plus the preview bulk variant) is ready to revive or resubmit once the overall approach is agreed, but the preview-create side needs API-level support to be atomic, and it makes sense to settle that design first rather than add more flags.

@github-project-automation github-project-automation Bot moved this from Untriaged to Done in workers-sdk Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Allow atomically setting a Worker's complete runtime env (vars + secrets) at deploy

2 participants