diff --git a/.changeset/calm-camels-return.md b/.changeset/calm-camels-return.md new file mode 100644 index 000000000000..1c7b44e40e96 --- /dev/null +++ b/.changeset/calm-camels-return.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +Add strict mode for the `wrangler deploy` command + +Add a new flag: `--strict` that makes the `wrangler deploy` command be more strict and not deploy workers when the deployment could be potentially problematic. This "strict mode" currently only affects non-interactive sessions where conflicts with the remote settings for the worker (for example when the worker has been re-deployed via the dashboard) will cause the deployment to fail instead of automatically overriding the remote settings. diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts index 8c2f844a9f02..0237dc0d9bda 100644 --- a/packages/wrangler/src/__tests__/deploy.test.ts +++ b/packages/wrangler/src/__tests__/deploy.test.ts @@ -14021,6 +14021,99 @@ export default{ return normalizedLog; } }); + + describe("with strict mode enabled", () => { + it("should error if there are remote config difference (with --x-remote-diff-check) in non-interactive mode", async () => { + setIsTTY(false); + + writeWorkerSource(); + mockGetServiceByName("test-name", "production", "dash"); + writeWranglerConfig( + { + compatibility_date: "2024-04-24", + main: "./index.js", + }, + "./wrangler.json" + ); + mockSubDomainRequest(); + mockUploadWorkerRequest(); + mockGetServiceBindings("test-name", []); + mockGetServiceRoutes("test-name", []); + mockGetServiceCustomDomainRecords([]); + mockGetServiceSubDomainData("test-name", { + enabled: true, + previews_enabled: false, + }); + mockGetServiceSchedules("test-name", { schedules: [] }); + mockGetServiceMetadata("test-name", { + created_on: "2025-08-07T09:34:47.846308Z", + modified_on: "2025-08-08T10:48:12.688997Z", + script: { + created_on: "2025-08-07T09:34:47.846308Z", + modified_on: "2025-08-08T10:48:12.688997Z", + id: "silent-firefly-dbe3", + observability: { enabled: true, head_sampling_rate: 1 }, + compatibility_date: "2024-04-24", + }, + } as unknown as ServiceMetadataRes["default_environment"]); + + await runWrangler("deploy --x-remote-diff-check --strict"); + + expect(std.warn).toMatchInlineSnapshot(` + "▲ [WARNING] The local configuration being used (generated from your local configuration file) differs from the remote configuration of your Worker set via the Cloudflare Dashboard: + + \\"bindings\\": [] + }, + \\"observability\\": { + - \\"enabled\\": true, + + \\"enabled\\": false, + \\"head_sampling_rate\\": 1, + \\"logs\\": { + \\"enabled\\": false, + + Deploying the Worker will override the remote configuration with your local one. + + " + `); + + expect(std.err).toMatchInlineSnapshot(` + "X [ERROR] Aborting the deployment operation because of conflicts. To override and deploy anyway remove the \`--strict\` flag + + " + `); + // note: the test and the wrangler run share the same process, and we expect the deploy command (which fails) + // to set a non-zero exit code + expect(process.exitCode).not.toBe(0); + }); + + it("should error when worker was last deployed from api", async () => { + setIsTTY(false); + + msw.use(...mswSuccessDeploymentScriptAPI); + writeWranglerConfig(); + writeWorkerSource(); + mockSubDomainRequest(); + mockUploadWorkerRequest(); + + await runWrangler("deploy ./index --strict"); + + expect(std.warn).toMatchInlineSnapshot(` + "▲ [WARNING] You are about to publish a Workers Service that was last updated via the script API. + + Edits that have been made via the script API will be overridden by your local code and config. + + " + `); + expect(std.err).toMatchInlineSnapshot(` + "X [ERROR] Aborting the deployment operation because of conflicts. To override and deploy anyway remove the \`--strict\` flag + + " + `); + // note: the test and the wrangler run share the same process, and we expect the deploy command (which fails) + // to set a non-zero exit code + expect(process.exitCode).not.toBe(0); + }); + }); }); /** Write mock assets to the file system so they can be uploaded. */ diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index e590029cb91a..0a8f2c847b94 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -33,6 +33,7 @@ import { } from "../environments"; import { UserError } from "../errors"; import { getFlag } from "../experimental-flags"; +import { isNonInteractiveOrCI } from "../is-interactive"; import { logger } from "../logger"; import { getMetricsUsageHeaders } from "../metrics"; import { isNavigatorDefined } from "../navigator-user-agent"; @@ -120,6 +121,7 @@ type Props = { experimentalAutoCreate: boolean; metafile: string | boolean | undefined; containersRollout: "immediate" | "gradual" | undefined; + strict: boolean | undefined; }; export type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute; @@ -357,6 +359,8 @@ export default async function deploy(props: Props): Promise<{ workerTag: string | null; targets?: string[]; }> { + const deployConfirm = getDeployConfirmFunction(props.strict); + // TODO: warn if git/hg has uncommitted changes const { config, accountId, name, entry } = props; let workerTag: string | null = null; @@ -417,7 +421,7 @@ export default async function deploy(props: Props): Promise<{ `\n${configDiff.diff}\n\n` + "Deploying the Worker will override the remote configuration with your local one." ); - if (!(await confirm("Would you like to continue?"))) { + if (!(await deployConfirm("Would you like to continue?"))) { return { versionId, workerTag }; } } @@ -425,7 +429,7 @@ export default async function deploy(props: Props): Promise<{ logger.warn( `You are about to publish a Workers Service that was last published via the Cloudflare Dashboard.\nEdits that have been made via the dashboard will be overridden by your local code and config.` ); - if (!(await confirm("Would you like to continue?"))) { + if (!(await deployConfirm("Would you like to continue?"))) { return { versionId, workerTag }; } } @@ -433,7 +437,7 @@ export default async function deploy(props: Props): Promise<{ logger.warn( `You are about to publish a Workers Service that was last updated via the script API.\nEdits that have been made via the script API will be overridden by your local code and config.` ); - if (!(await confirm("Would you like to continue?"))) { + if (!(await deployConfirm("Would you like to continue?"))) { return { versionId, workerTag }; } } @@ -1430,3 +1434,21 @@ export async function updateQueueConsumers( return updateConsumers; } + +function getDeployConfirmFunction( + strictMode = false +): (text: string) => Promise { + const nonInteractive = isNonInteractiveOrCI(); + + if (nonInteractive && strictMode) { + return async () => { + logger.error( + "Aborting the deployment operation because of conflicts. To override and deploy anyway remove the `--strict` flag" + ); + process.exitCode = 1; + return false; + }; + } + + return confirm; +} diff --git a/packages/wrangler/src/deploy/index.ts b/packages/wrangler/src/deploy/index.ts index fb15ecbb35c6..ef6b0a6fcf66 100644 --- a/packages/wrangler/src/deploy/index.ts +++ b/packages/wrangler/src/deploy/index.ts @@ -232,11 +232,17 @@ export const deployCommand = createCommand({ choices: ["immediate", "gradual"] as const, }, "experimental-deploy-remote-diff-check": { - describe: `Experimental: Enable The Deployment Remote Diff check`, + describe: "Experimental: Enable The Deployment Remote Diff check", type: "boolean", hidden: true, alias: ["x-remote-diff-check"], }, + strict: { + describe: + "Enables strict mode for the deploy command, this prevents deployments to occur when there are even small potential risks.", + type: "boolean", + default: false, + }, }, behaviour: { useConfigRedirectIfAvailable: true, @@ -251,7 +257,7 @@ export const deployCommand = createCommand({ validateArgs(args) { if (args.nodeCompat) { throw new UserError( - `The --node-compat flag is no longer supported as of Wrangler v4. Instead, use the \`nodejs_compat\` compatibility flag. This includes the functionality from legacy \`node_compat\` polyfills and natively implemented Node.js APIs. See https://developers.cloudflare.com/workers/runtime-apis/nodejs for more information.`, + "The --node-compat flag is no longer supported as of Wrangler v4. Instead, use the `nodejs_compat` compatibility flag. This includes the functionality from legacy `node_compat` polyfills and natively implemented Node.js APIs. See https://developers.cloudflare.com/workers/runtime-apis/nodejs for more information.", { telemetryMessage: true } ); } @@ -382,6 +388,7 @@ export const deployCommand = createCommand({ dispatchNamespace: args.dispatchNamespace, experimentalAutoCreate: args.experimentalAutoCreate, containersRollout: args.containersRollout, + strict: args.strict, }); writeOutput({