From aa18a35e9d4df6a640bff6f73f58242e4721f060 Mon Sep 17 00:00:00 2001 From: Daniel Rivas <1887507+danielrs@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:49:36 -0500 Subject: [PATCH] feat: wrangler deploy prompts warning with deployment --- .../wrangler/src/__tests__/deploy.test.ts | 33 +++++++ .../__tests__/deprecated-usage-model.test.ts | 11 ++- .../helpers/msw/handlers/versions.ts | 97 +++++++++++++++++++ packages/wrangler/src/deploy/deploy.ts | 14 ++- packages/wrangler/src/versions/deploy.ts | 66 +++++++++++++ 5 files changed, 218 insertions(+), 3 deletions(-) diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts index c57fb1291ae7..a9c731a14ba2 100644 --- a/packages/wrangler/src/__tests__/deploy.test.ts +++ b/packages/wrangler/src/__tests__/deploy.test.ts @@ -43,6 +43,10 @@ import { mswSuccessOauthHandlers, mswSuccessUserHandlers, } from "./helpers/msw"; +import { + mswListNewDeploymentsLatestFiftyFifty, + mswListNewDeploymentsLatestFull, +} from "./helpers/msw/handlers/versions"; import { runInTempDir } from "./helpers/run-in-tmp"; import { runWrangler } from "./helpers/run-wrangler"; import { writeWorkerSource } from "./helpers/write-worker-source"; @@ -78,6 +82,7 @@ describe("deploy", () => { setIsTTY(true); mockLastDeploymentRequest(); mockDeploymentsListRequest(); + msw.use(...mswListNewDeploymentsLatestFull); logger.loggerLevel = "log"; }); @@ -423,6 +428,34 @@ describe("deploy", () => { `); }); + it("should warn user when worker has deployment with multiple versions", async () => { + msw.use(...mswListNewDeploymentsLatestFiftyFifty); + writeWranglerToml(); + writeWorkerSource(); + mockSubDomainRequest(); + mockUploadWorkerRequest(); + mockConfirm({ + text: `"wrangler deploy" will upload a new version and deploy it globally immediately.\nAre you sure you want to continue?`, + result: false, + }); + + await runWrangler("deploy ./index"); + + expect(std.warn).toMatchInlineSnapshot(` + "▲ [WARNING] Your last deployment has multiple versions. To progress this deployment use \\"wrangler versions deploy\\" instead. + + Currently deployed versions: + + Version(s): (50%) test-name:version:0 + Created: 2021-01-01T00:00:00.000Z + + (50%) test-name:version:1 + Created: 2021-01-01T00:00:00.000Z + + " + `); + }); + it("should warn user when additional properties are passed to a services config", async () => { writeWranglerToml({ d1_databases: [ diff --git a/packages/wrangler/src/__tests__/deprecated-usage-model.test.ts b/packages/wrangler/src/__tests__/deprecated-usage-model.test.ts index aa25d0273057..3e5ef2472277 100644 --- a/packages/wrangler/src/__tests__/deprecated-usage-model.test.ts +++ b/packages/wrangler/src/__tests__/deprecated-usage-model.test.ts @@ -4,6 +4,7 @@ import { mockConsoleMethods } from "./helpers/mock-console"; import { mockUploadWorkerRequest } from "./helpers/mock-upload-worker"; import { mockSubDomainRequest } from "./helpers/mock-workers-subdomain"; import { msw, mswSuccessDeploymentScriptMetadata } from "./helpers/msw"; +import { mswListNewDeploymentsLatestFull } from "./helpers/msw/handlers/versions"; import { runInTempDir } from "./helpers/run-in-tmp"; import { runWrangler } from "./helpers/run-wrangler"; import { writeWorkerSource } from "./helpers/write-worker-source"; @@ -26,7 +27,10 @@ describe("deprecated-usage-model", () => { }); it("should warn user about ignored usage model if usage_model specified", async () => { - msw.use(...mswSuccessDeploymentScriptMetadata); + msw.use( + ...mswSuccessDeploymentScriptMetadata, + ...mswListNewDeploymentsLatestFull + ); writeWranglerToml({ usage_model: "bundled" }); writeWorkerSource(); mockSubDomainRequest(); @@ -41,7 +45,10 @@ describe("deprecated-usage-model", () => { `); }); it("should not warn user about ignored usage model if usage_model not specified", async () => { - msw.use(...mswSuccessDeploymentScriptMetadata); + msw.use( + ...mswSuccessDeploymentScriptMetadata, + ...mswListNewDeploymentsLatestFull + ); writeWranglerToml(); writeWorkerSource(); mockSubDomainRequest(); diff --git a/packages/wrangler/src/__tests__/helpers/msw/handlers/versions.ts b/packages/wrangler/src/__tests__/helpers/msw/handlers/versions.ts index ecd7bc3c1312..8dea63eaa3fc 100644 --- a/packages/wrangler/src/__tests__/helpers/msw/handlers/versions.ts +++ b/packages/wrangler/src/__tests__/helpers/msw/handlers/versions.ts @@ -1,5 +1,102 @@ import { http, HttpResponse } from "msw"; import { createFetchResult } from "../index"; +import type { ApiDeployment, ApiVersion } from "../../../../versions/types"; + +export const mswListNewDeploymentsLatestFull = [ + http.get( + "*/accounts/:accountId/workers/scripts/:scriptName/deployments", + ({ params }) => { + return HttpResponse.json( + createFetchResult({ + deployments: [ + { + id: `deployment:${params["scriptName"]}`, + source: "api", + strategy: "percentage", + author_email: "author@example.com", + created_on: "2021-01-01T00:00:00.000000Z", + versions: [ + { + version_id: `${params["scriptName"]}:version:0`, + percentage: 100.0, + }, + ], + }, + ] as Array, + }) + ); + }, + { once: true } + ), + http.get( + "*/accounts/:accountId/workers/scripts/:scriptName/versions/:version", + ({ params }) => { + return HttpResponse.json( + createFetchResult({ + id: params["version"], + number: 1, + metadata: { + created_on: "2021-01-01T00:00:00.000000Z", + modified_on: "2021-01-01T00:00:00.000000Z", + source: "api", + author_email: "author@example.com", + }, + } as ApiVersion) + ); + }, + { once: false } + ), +]; + +export const mswListNewDeploymentsLatestFiftyFifty = [ + http.get( + "*/accounts/:accountId/workers/scripts/:scriptName/deployments", + ({ params }) => { + return HttpResponse.json( + createFetchResult({ + deployments: [ + { + id: `deployment:${params["scriptName"]}`, + source: "api", + strategy: "percentage", + author_email: "author@example.com", + created_on: "2021-01-01T00:00:00.000000Z", + versions: [ + { + version_id: `${params["scriptName"]}:version:0`, + percentage: 50.0, + }, + { + version_id: `${params["scriptName"]}:version:1`, + percentage: 50.0, + }, + ], + }, + ] as Array, + }) + ); + }, + { once: true } + ), + http.get( + "*/accounts/:accountId/workers/scripts/:scriptName/versions/:version", + ({ params }) => { + return HttpResponse.json( + createFetchResult({ + id: params["version"], + number: 1, + metadata: { + created_on: "2021-01-01T00:00:00.000000Z", + modified_on: "2021-01-01T00:00:00.000000Z", + source: "api", + author_email: "author@example.com", + }, + } as ApiVersion) + ); + }, + { once: false } + ), +]; export const mswListNewDeployments = http.get( "*/accounts/:accountId/workers/scripts/:workerName/deployments", diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index aec7003c676e..f50c149e9922 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -2,6 +2,7 @@ import assert from "node:assert"; import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { URLSearchParams } from "node:url"; +import { cancel } from "@cloudflare/cli"; import { fetchListResult, fetchResult } from "../cfetch"; import { printBindings } from "../config"; import { bundleWorker } from "../deployment-bundle/bundle"; @@ -45,6 +46,7 @@ import { } from "../sourcemap"; import triggersDeploy from "../triggers/deploy"; import { logVersionIdChange } from "../utils/deployment-id-version-id-change"; +import { confirmLatestDeploymentOverwrite } from "../versions/deploy"; import { getZoneForRoute } from "../zones"; import type { Config } from "../config"; import type { @@ -421,7 +423,8 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m const envName = props.env ?? "production"; const start = Date.now(); - const notProd = Boolean(!props.legacyEnv && props.env); + const prod = Boolean(props.legacyEnv || !props.env); + const notProd = !prod; const workerName = notProd ? `${scriptName} (${envName})` : scriptName; const workerUrl = props.dispatchNamespace ? `/accounts/${accountId}/workers/dispatch/namespaces/${props.dispatchNamespace}/scripts/${scriptName}` @@ -433,6 +436,14 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m const { format } = props.entry; + if (!props.dispatchNamespace && prod && accountId && scriptName) { + const yes = await confirmLatestDeploymentOverwrite(accountId, scriptName); + if (!yes) { + cancel("Aborting deploy..."); + return; + } + } + if ( !props.isWorkersSite && Boolean(props.assetPaths) && @@ -460,6 +471,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m "You cannot configure [data_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[rules]` in your wrangler.toml" ); } + try { if (props.noBundle) { // if we're not building, let's just copy the entry to the destination directory diff --git a/packages/wrangler/src/versions/deploy.ts b/packages/wrangler/src/versions/deploy.ts index 372c4a402c66..722d6083473d 100644 --- a/packages/wrangler/src/versions/deploy.ts +++ b/packages/wrangler/src/versions/deploy.ts @@ -9,14 +9,18 @@ import { spinnerWhile, } from "@cloudflare/cli/interactive"; import { findWranglerToml, readConfig } from "../config"; +import { confirm } from "../dialogs"; import { UserError } from "../errors"; +import { logger } from "../logger"; import * as metrics from "../metrics"; +import { APIError } from "../parse"; import { printWranglerBanner } from "../update-check"; import { requireAuth } from "../user"; import formatLabelledValues from "../utils/render-labelled-values"; import { createDeployment, fetchDeployableVersions, + fetchLatestDeployment, fetchLatestDeploymentVersions, fetchVersions, patchNonVersionedScriptSettings, @@ -205,6 +209,68 @@ function getConfig( return config; } +/** + * Prompts the user for confirmation when overwriting the latest deployment, given that it's split. + */ +export async function confirmLatestDeploymentOverwrite( + accountId: string, + scriptName: string +) { + try { + const latest = await fetchLatestDeployment(accountId, scriptName); + if (latest && latest.versions.length >= 2) { + // Fetch the version details. + + const versionCache: VersionCache = new Map(); + const versionIds = latest.versions.map((v) => v.version_id); + await fetchVersions(accountId, scriptName, versionCache, ...versionIds); + + // Format each version. + + const formattedVersions = latest.versions.map((traffic) => { + const version = versionCache.get(traffic.version_id); + assert(version); + const percentage = brandColor(`(${traffic.percentage}%)`); + const details = formatLabelledValues( + { Created: new Date(version.metadata["created_on"]).toISOString() }, + { + indentationCount: 4, + labelJustification: "right", + formatLabel: (label) => gray(label + ":"), + formatValue: (value) => gray(value), + } + ); + return `${percentage} ${version.id}\n${details}`; + }); + + // Format deployment. + + const formattedDeployment = formatLabelledValues({ + "Version(s)": formattedVersions.join("\n\n"), + }); + + // Print message and confirmation. + + logger.warn( + `Your last deployment has multiple versions. To progress this deployment use "wrangler versions deploy" instead. +Currently deployed versions: + +${formattedDeployment}` + ); + + return await confirm( + `"wrangler deploy" will upload a new version and deploy it globally immediately.\nAre you sure you want to continue?` + ); + } + } catch (e) { + const isNotFound = e instanceof APIError && e.code == 10007; + if (!isNotFound) { + throw e; + } + } + return true; +} + export async function printLatestDeployment( accountId: string, workerName: string,