Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 80 additions & 22 deletions packages/deploy-helpers/src/preview/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ async function prepareContainersForPreview(
}

export const NO_ACTIVE_PREVIEW_URLS_MESSAGE =
"Note: This Preview deployment has no active URLs. To get one, enable Preview Deployments on workers.dev or a custom domain. See https://developers.cloudflare.com/workers/previews/custom-domains/ for more information";
"Note: This Preview deployment was created, but it has no active URLs.\n\nEnable at least one Preview URL, then run `wrangler deploy` to apply routing:\n\n- workers.dev: set `preview_urls` to `true` in your Wrangler config. If `preview_urls` is omitted, it follows `workers_dev`.\n- Custom domain: set `custom_domain` and `previews_enabled` to `true` on a route.\n\nYou can also enable Preview URLs in the Cloudflare dashboard. Choose one source of truth for routes. If Wrangler manages this Worker, keep these settings in your Wrangler config so your next deploy does not turn them off.\n\nDocs:\n- workers.dev Previews: https://developers.cloudflare.com/workers/previews/custom-domains/#enable-workersdev-previews\n- Custom domain Previews: https://developers.cloudflare.com/workers/previews/custom-domains/#enable-custom-domain-previews";

function getPreviewMigrationsToUpload(
workerName: string,
Expand Down Expand Up @@ -570,17 +570,23 @@ function formatPreviewDeploymentSummary(
previewResource: PreviewResource,
deployment: DeploymentResource,
isNew: boolean,
hasWarnings = false,
pullRequest?: PullRequestMetadata
): string {
const statusLabel = isNew ? chalk.green("(new)") : chalk.dim("(updated)");
const hasActiveUrls =
(previewResource.urls?.length ?? 0) > 0 ||
(deployment.urls?.length ?? 0) > 0;
const statusLabel =
hasWarnings || !hasActiveUrls
? chalk.yellow(`(${isNew ? "new" : "updated"} with warnings)`)
: isNew
? chalk.green("(new)")
: chalk.dim("(updated)");
const pullRequestUrl =
deployment.annotations?.["workers/pull_request_url"] ?? pullRequest?.url;
const pullRequestNumber =
deployment.annotations?.["workers/pull_request_number"] ??
pullRequest?.number;
const hasActiveUrls =
(previewResource.urls?.length ?? 0) > 0 ||
(deployment.urls?.length ?? 0) > 0;

return [
`${chalk.bold("Preview:")} ${previewResource.name} ${statusLabel}`,
Expand All @@ -599,11 +605,48 @@ function formatPreviewDeploymentSummary(
].join("\n");
}

const previewProductionResourceBindingTypes = new Set([
"kv_namespace",
"d1",
"r2_bucket",
"queue",
"workflow",
"hyperdrive",
"vectorize",
"service",
]);

const listFormatter = new Intl.ListFormat("en-US");

function formatPreviewResourceWarning(
missingBindings: Record<string, { type: string }>
): string {
const missingResourceTypes = [
...new Set(
Object.values(missingBindings)
.map((binding) => binding.type)
.filter((type) => previewProductionResourceBindingTypes.has(type))
.map((type) =>
getBindingTypeFriendlyName(
type as Parameters<typeof getBindingTypeFriendlyName>[0]
)
)
),
];

if (missingResourceTypes.length === 0) {
return "Use Preview-safe values instead of production values.";
}

return `Do not reuse production resources for ${listFormatter.format(missingResourceTypes)} unless you intentionally want Preview traffic to share production data.`;
}

function logMissingPreviewsBindingsWarning(
topLevelBindings: Record<string, { type: string }>,
remotePreviewDefaultBindings: Record<string, Binding> | undefined,
localPreviewBindings: Record<string, Binding>
) {
localPreviewBindings: Record<string, Binding>,
hasActiveUrls: boolean
): boolean {
const availableBindingNames = new Set([
...Object.keys(remotePreviewDefaultBindings ?? {}),
...Object.keys(localPreviewBindings),
Expand All @@ -615,11 +658,14 @@ function logMissingPreviewsBindingsWarning(
);

if (Object.keys(missingBindings).length === 0) {
return;
return false;
}

logger.warn(`Your configuration has diverged.
The following bindings are configured at the top level of your Wrangler config file, but are missing from the Previews settings of your Worker.
logger.warn(`Preview deployment created, but its runtime configuration is incomplete.

${hasActiveUrls ? "Your Preview URL is live, but requests may fail or behave differently from production." : "Requests may fail or behave differently from production when this Preview has an active URL."}

These bindings exist in your top-level Wrangler config, but are missing from this Worker's Preview configuration:

${Object.entries(missingBindings)
.map(
Expand All @@ -628,7 +674,13 @@ ${Object.entries(missingBindings)
)
.join("\n")}

Either include these bindings in the ${chalk.cyan(`"previews"`)} field of your Wrangler config or update the Previews settings of your Worker in the Cloudflare dashboard.`);
Fix: add these bindings to the ${chalk.cyan(`"previews"`)} field in your Wrangler config using Preview-safe values.

${formatPreviewResourceWarning(missingBindings)}

Docs: https://developers.cloudflare.com/workers/previews/configuration/#wrangler-configuration-file`);

return true;
}

/**
Expand Down Expand Up @@ -822,28 +874,34 @@ export async function preview(
JSON.stringify({ preview: previewResource, deployment }, null, 2)
);
} else {
logger.log(
formatPreviewDeploymentSummary(
previewResource,
deployment,
isNewPreview,
pullRequest
)
);

let hasWarnings = false;
const hasActiveUrls =
(previewResource.urls?.length ?? 0) > 0 ||
(deployment.urls?.length ?? 0) > 0;
const topLevelBindings = getBindings(config);
if (Object.keys(topLevelBindings).length > 0) {
const previewDefaults = await getWorkerPreviewDefaults(
config,
accountId,
workerName
);
logMissingPreviewsBindingsWarning(
hasWarnings = logMissingPreviewsBindingsWarning(
topLevelBindings,
previewDefaults.env,
extractConfigBindings(config)
extractConfigBindings(config),
hasActiveUrls
);
}

logger.log(
formatPreviewDeploymentSummary(
previewResource,
deployment,
isNewPreview,
hasWarnings,
pullRequest
)
);
}

return { preview: previewResource, deployment, isNewPreview };
Expand Down
45 changes: 45 additions & 0 deletions packages/wrangler/src/__tests__/preview-version-warning.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, test } from "vitest";
import {
formatWranglerPreviewVersionWarning,
isWranglerPreviewVersionUnsupported,
warnIfWranglerPreviewVersionUnsupported,
} from "../preview/version-warning";
import { mockConsoleMethods } from "./helpers/mock-console";

describe("wrangler preview version warning", () => {
const std = mockConsoleMethods();

test("requires Wrangler 4.127.1 or later", ({ expect }) => {
expect(isWranglerPreviewVersionUnsupported("4.127.0")).toBe(true);
expect(isWranglerPreviewVersionUnsupported("4.127.1")).toBe(false);
expect(isWranglerPreviewVersionUnsupported("4.128.0")).toBe(false);
});

test("points users at the project-local Wrangler install", ({ expect }) => {
expect(formatWranglerPreviewVersionWarning("4.123.0"))
.toMatchInlineSnapshot(`
"Workers Previews require Wrangler 4.127.1 or later. This project is using Wrangler 4.123.0.

\`npx wrangler preview\` uses the Wrangler installed in this project, not your global Wrangler.

Update this project:
npm install -D wrangler@latest @cloudflare/workers-types@latest

Or run once:
npx wrangler@latest preview"
`);
});

test("warns when the Wrangler version is unsupported", ({ expect }) => {
expect(warnIfWranglerPreviewVersionUnsupported("4.123.0")).toBe(true);
expect(std.warn).toContain(
"Workers Previews require Wrangler 4.127.1 or later."
);
expect(std.warn).toContain("npm install -D wrangler@latest");
});

test("does not warn when the Wrangler version is supported", ({ expect }) => {
expect(warnIfWranglerPreviewVersionUnsupported("4.127.1")).toBe(false);
expect(std.warn).toBe("");
});
});
2 changes: 1 addition & 1 deletion packages/wrangler/src/__tests__/preview.secret.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const BRANCH_ENV_VARS = [
"CI_COMMIT_REF_NAME",
] as const;
const NO_ACTIVE_PREVIEW_URLS_MESSAGE =
"Note: This Preview deployment has no active URLs. To get one, enable Preview Deployments on workers.dev or a custom domain. See https://developers.cloudflare.com/workers/previews/custom-domains/ for more information";
"Note: This Preview deployment was created, but it has no active URLs.\n\nEnable at least one Preview URL, then run `wrangler deploy` to apply routing:\n\n- workers.dev: set `preview_urls` to `true` in your Wrangler config. If `preview_urls` is omitted, it follows `workers_dev`.\n- Custom domain: set `custom_domain` and `previews_enabled` to `true` on a route.\n\nYou can also enable Preview URLs in the Cloudflare dashboard. Choose one source of truth for routes. If Wrangler manages this Worker, keep these settings in your Wrangler config so your next deploy does not turn them off.\n\nDocs:\n- workers.dev Previews: https://developers.cloudflare.com/workers/previews/custom-domains/#enable-workersdev-previews\n- Custom domain Previews: https://developers.cloudflare.com/workers/previews/custom-domains/#enable-custom-domain-previews";

async function withoutBranchEnvVars<T>(callback: () => Promise<T>): Promise<T> {
const originalBranchEnv = Object.fromEntries(
Expand Down
59 changes: 51 additions & 8 deletions packages/wrangler/src/__tests__/preview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,16 @@ describe("wrangler preview", () => {
main: "src/index.ts",
compatibility_date: "2025-01-01",
kv_namespaces: [{ binding: "IMPORTANT_BINDING", id: "kv-id-123" }],
d1_databases: [
{
binding: "DB",
database_name: "production-db",
database_id: "production-db-id",
},
],
hyperdrive: [{ binding: "HYPERDRIVE", id: "production-db" }],
vectorize: [{ binding: "VECTORIZE", index_name: "production-index" }],
services: [{ binding: "API", service: "production-api" }],
})
);

Expand Down Expand Up @@ -1201,18 +1211,33 @@ describe("wrangler preview", () => {
const normalizedWarningOutput = warningOutput.replace(/\s+/g, " ");

expect(normalizedWarningOutput).toContain(
"Your configuration has diverged."
"Preview deployment created, but its runtime configuration is incomplete."
);
expect(normalizedWarningOutput).toContain(
"Your Preview URL is live, but requests may fail or behave differently from production."
);
expect(normalizedWarningOutput).toContain(
"These bindings exist in your top-level Wrangler config, but are missing from this Worker's Preview configuration:"
);
expect(normalizedWarningOutput).toContain(
"IMPORTANT_BINDING KV Namespace"
);
expect(normalizedWarningOutput).toContain("DB D1 Database");
expect(normalizedWarningOutput).toContain("HYPERDRIVE Hyperdrive Config");
expect(normalizedWarningOutput).toContain("VECTORIZE Vectorize Index");
expect(normalizedWarningOutput).toContain("API Worker");
expect(normalizedWarningOutput).toContain(
"The following bindings are configured at the top level of your Wrangler config file, but are missing from the Previews settings of your Worker."
'Fix: add these bindings to the "previews" field in your Wrangler config using Preview-safe values.'
);
expect(warningOutput).toContain("IMPORTANT_BINDING");
expect(warningOutput).toContain("KV Namespace");
expect(normalizedWarningOutput).toContain(
'Either include these bindings in the "previews" field of your Wrangler config'
"Do not reuse production resources for KV Namespace, D1 Database, Hyperdrive Config, Vectorize Index, and Worker unless you intentionally want Preview traffic to share production data."
);
expect(normalizedWarningOutput).toContain(
"or update the Previews settings of your Worker in the Cloudflare dashboard."
"Docs: https://developers.cloudflare.com/workers/previews/configuration/#wrangler-configuration-file"
);
expect(std.out).toContain("Preview: test-preview (new with warnings)");
expect(std.out).toContain(
"Preview URL: https://test-preview.test-worker.cloudflare.app"
);
});

Expand Down Expand Up @@ -1941,11 +1966,29 @@ describe("wrangler preview", () => {
(line) => line.startsWith("Preview") || line.startsWith("Deployment")
);
expect(summaryLines).toEqual([
"Preview: empty-urls-preview (new)",
"Preview: empty-urls-preview (new with warnings)",
"Deployment ID: deployment-id-empty-urls",
]);
expect(std.out).toContain(
"Note: This Preview deployment has no active URLs. To get one, enable Preview Deployments on workers.dev or a custom domain. See https://developers.cloudflare.com/workers/previews/custom-domains/ for more information"
"Note: This Preview deployment was created, but it has no active URLs."
);
expect(std.out).toContain(
"Enable at least one Preview URL, then run `wrangler deploy` to apply routing:"
);
expect(std.out).toContain(
"workers.dev: set `preview_urls` to `true` in your Wrangler config. If `preview_urls` is omitted, it follows `workers_dev`."
);
expect(std.out).toContain(
"Custom domain: set `custom_domain` and `previews_enabled` to `true` on a route."
);
expect(std.out).toContain(
"Choose one source of truth for routes. If Wrangler manages this Worker, keep these settings in your Wrangler config so your next deploy does not turn them off."
);
expect(std.out).toContain(
"workers.dev Previews: https://developers.cloudflare.com/workers/previews/custom-domains/#enable-workersdev-previews"
);
expect(std.out).toContain(
"Custom domain Previews: https://developers.cloudflare.com/workers/previews/custom-domains/#enable-custom-domain-previews"
);
});

Expand Down
3 changes: 3 additions & 0 deletions packages/wrangler/src/preview/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { cleanupDestination } from "../deployment-bundle/merge-config-args";
import { writeOutput } from "../output";
import { requireAuth } from "../user";
import { deployPreviewContainers, verifyContainersScope } from "./containers";
import { warnIfWranglerPreviewVersionUnsupported } from "./version-warning";

export const previewCommand = createCommand({
metadata: {
Expand Down Expand Up @@ -63,6 +64,8 @@ export const previewCommand = createCommand({
suggestSkillsAfterHandler: (args) => args.json !== true,
},
handler: async function previewHandler(args, { config }) {
warnIfWranglerPreviewVersionUnsupported();

const accountId = await requireAuth(config);

const entry = await getEntry({ script: args.script }, config, "deploy");
Expand Down
36 changes: 36 additions & 0 deletions packages/wrangler/src/preview/version-warning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import semiver from "semiver";
import { version as wranglerVersion } from "../../package.json";
import { logger } from "../logger";

export const MIN_WRANGLER_PREVIEW_VERSION = "4.127.1";

export function isWranglerPreviewVersionUnsupported(
version = wranglerVersion
): boolean {
return semiver(version, MIN_WRANGLER_PREVIEW_VERSION) < 0;
}

export function formatWranglerPreviewVersionWarning(
version = wranglerVersion
): string {
return `Workers Previews require Wrangler ${MIN_WRANGLER_PREVIEW_VERSION} or later. This project is using Wrangler ${version}.

\`npx wrangler preview\` uses the Wrangler installed in this project, not your global Wrangler.

Update this project:
npm install -D wrangler@latest @cloudflare/workers-types@latest

Or run once:
npx wrangler@latest preview`;
}

export function warnIfWranglerPreviewVersionUnsupported(
version = wranglerVersion
): boolean {
if (!isWranglerPreviewVersionUnsupported(version)) {
return false;
}

logger.warn(formatWranglerPreviewVersionWarning(version));
return true;
}
Loading