diff --git a/packages/wrangler/e2e/versions.test.ts b/packages/wrangler/e2e/versions.test.ts index 67a460d60ce5..7f7ace7c6632 100644 --- a/packages/wrangler/e2e/versions.test.ts +++ b/packages/wrangler/e2e/versions.test.ts @@ -555,7 +555,8 @@ describe("versions deploy", { timeout: TIMEOUT }, () => { ); expect(normalize(upload.output)).toMatchInlineSnapshot(` - "X [ERROR] Legacy assets does not support uploading versions through \`wrangler versions upload\`. You must use \`wrangler deploy\` instead. + "▲ [WARNING] 🚧 \`wrangler versions upload\` is an open-beta command. Please report any issues to https://github.com/cloudflare/workers-sdk/issues/new/choose + X [ERROR] Legacy assets does not support uploading versions through \`wrangler versions upload\`. You must use \`wrangler deploy\` instead. 🪵 Logs were written to """ `); }); @@ -589,7 +590,8 @@ describe("versions deploy", { timeout: TIMEOUT }, () => { const upload = await helper.run(`wrangler versions upload --x-versions`); expect(normalize(upload.output)).toMatchInlineSnapshot(` - "X [ERROR] Workers Sites does not support uploading versions through \`wrangler versions upload\`. You must use \`wrangler deploy\` instead. + "▲ [WARNING] 🚧 \`wrangler versions upload\` is an open-beta command. Please report any issues to https://github.com/cloudflare/workers-sdk/issues/new/choose + X [ERROR] Workers Sites does not support uploading versions through \`wrangler versions upload\`. You must use \`wrangler deploy\` instead. 🪵 Logs were written to """ `); }); diff --git a/packages/wrangler/src/__tests__/versions/versions.deploy.test.ts b/packages/wrangler/src/__tests__/versions/versions.deploy.test.ts index 078aec197c8e..29f93f21c26c 100644 --- a/packages/wrangler/src/__tests__/versions/versions.deploy.test.ts +++ b/packages/wrangler/src/__tests__/versions/versions.deploy.test.ts @@ -1,11 +1,9 @@ -import yargs from "yargs"; import { normalizeOutput } from "../../../e2e/helpers/normalize"; import { assignAndDistributePercentages, parseVersionSpecs, summariseVersionTraffic, validateTrafficSubtotal, - versionsDeployOptions, } from "../../versions/deploy"; import { collectCLIOutput } from "../helpers/collect-cli-output"; import { mockAccountId, mockApiToken } from "../helpers/mock-account-id"; @@ -30,7 +28,6 @@ import { runInTempDir } from "../helpers/run-in-tmp"; import { runWrangler } from "../helpers/run-wrangler"; import { writeWorkerSource } from "../helpers/write-worker-source"; import { writeWranglerConfig } from "../helpers/write-wrangler-config"; -import type { VersionsDeployArgs } from "../../versions/deploy"; describe("versions deploy", () => { mockAccountId(); @@ -925,39 +922,28 @@ describe("versions deploy", () => { describe("units", () => { describe("parseVersionSpecs", () => { - const options = yargs().command( - "versions deploy [version-specs..]", - "", - // @ts-expect-error creating the command using a fresh yargs() but it expects one preconfigured with global options - versionsDeployOptions, - () => {} - ); - test("no args", () => { - const input = "versions deploy"; - - const args = options.parse(input) as VersionsDeployArgs; - const result = parseVersionSpecs(args); + const result = parseVersionSpecs({}); expect(result).toMatchObject(new Map()); }); test("1 positional arg", () => { - const input = "versions deploy 10000000-0000-0000-0000-000000000000@10%"; - - const args = options.parse(input) as VersionsDeployArgs; - const result = parseVersionSpecs(args); + const result = parseVersionSpecs({ + versionSpecs: ["10000000-0000-0000-0000-000000000000@10%"], + }); expect(Object.fromEntries(result)).toMatchObject({ "10000000-0000-0000-0000-000000000000": 10, }); }); test("2 positional args", () => { - const input = - "versions deploy 10000000-0000-0000-0000-000000000000@10% 20000000-0000-0000-0000-000000000000@90%"; - - const args = options.parse(input) as VersionsDeployArgs; - const result = parseVersionSpecs(args); + const result = parseVersionSpecs({ + versionSpecs: [ + "10000000-0000-0000-0000-000000000000@10%", + "20000000-0000-0000-0000-000000000000@90%", + ], + }); expect(Object.fromEntries(result)).toMatchObject({ "10000000-0000-0000-0000-000000000000": 10, @@ -966,34 +952,23 @@ describe("units", () => { }); test("1 pair of named args", () => { - const input = - "versions deploy --version-id 10000000-0000-0000-0000-000000000000 --percentage 10"; - - const args = options.parse(input) as VersionsDeployArgs; - const result = parseVersionSpecs(args); + const result = parseVersionSpecs({ + percentage: [10], + versionId: ["10000000-0000-0000-0000-000000000000"], + }); expect(Object.fromEntries(result)).toMatchObject({ "10000000-0000-0000-0000-000000000000": 10, }); }); test("2 pairs of named args", () => { - const input = - "versions deploy --version-id 10000000-0000-0000-0000-000000000000 --percentage 10 --version-id 20000000-0000-0000-0000-000000000000 --percentage 90"; - - const args = options.parse(input) as VersionsDeployArgs; - const result = parseVersionSpecs(args); - - expect(Object.fromEntries(result)).toMatchObject({ - "10000000-0000-0000-0000-000000000000": 10, - "20000000-0000-0000-0000-000000000000": 90, + const result = parseVersionSpecs({ + percentage: [10, 90], + versionId: [ + "10000000-0000-0000-0000-000000000000", + "20000000-0000-0000-0000-000000000000", + ], }); - }); - test("unordered named args", () => { - const input = - "versions deploy --version-id 10000000-0000-0000-0000-000000000000 --version-id 20000000-0000-0000-0000-000000000000 --percentage 10 --percentage 90"; - - const args = options.parse(input) as VersionsDeployArgs; - const result = parseVersionSpecs(args); expect(Object.fromEntries(result)).toMatchObject({ "10000000-0000-0000-0000-000000000000": 10, @@ -1001,23 +976,13 @@ describe("units", () => { }); }); test("unpaired named args", () => { - const input = - "versions deploy --version-id 10000000-0000-0000-0000-000000000000 --percentage 10 --version-id 20000000-0000-0000-0000-000000000000"; - - const args = options.parse(input) as VersionsDeployArgs; - const result = parseVersionSpecs(args); - - expect(Object.fromEntries(result)).toMatchObject({ - "10000000-0000-0000-0000-000000000000": 10, - "20000000-0000-0000-0000-000000000000": null, + const result = parseVersionSpecs({ + percentage: [10], + versionId: [ + "10000000-0000-0000-0000-000000000000", + "20000000-0000-0000-0000-000000000000", + ], }); - }); - test("unpaired, unordered named args", () => { - const input = - "versions deploy --version-id 10000000-0000-0000-0000-000000000000 --version-id 20000000-0000-0000-0000-000000000000 --percentage 10"; - - const args = options.parse(input) as VersionsDeployArgs; - const result = parseVersionSpecs(args); expect(Object.fromEntries(result)).toMatchObject({ "10000000-0000-0000-0000-000000000000": 10, diff --git a/packages/wrangler/src/__tests__/versions/versions.help.test.ts b/packages/wrangler/src/__tests__/versions/versions.help.test.ts index 396801519898..61e51721eec1 100644 --- a/packages/wrangler/src/__tests__/versions/versions.help.test.ts +++ b/packages/wrangler/src/__tests__/versions/versions.help.test.ts @@ -63,10 +63,10 @@ describe("versions --help", () => { 🫧 List, view, upload and deploy Versions of your Worker to Cloudflare COMMANDS - wrangler versions view View the details of a specific version of your Worker [beta] - wrangler versions list List the 10 most recent Versions of your Worker [beta] - wrangler versions upload Uploads your Worker code and config as a new Version [beta] - wrangler versions deploy [version-specs..] Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [beta] + wrangler versions view View the details of a specific version of your Worker [open-beta] + wrangler versions list List the 10 most recent Versions of your Worker [open-beta] + wrangler versions upload Uploads your Worker code and config as a new Version [open-beta] + wrangler versions deploy [version-specs..] Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [open-beta] wrangler versions secret Generate a secret that can be referenced in a Worker GLOBAL FLAGS @@ -88,10 +88,10 @@ describe("versions --help", () => { 🫧 List, view, upload and deploy Versions of your Worker to Cloudflare COMMANDS - wrangler versions view View the details of a specific version of your Worker [beta] - wrangler versions list List the 10 most recent Versions of your Worker [beta] - wrangler versions upload Uploads your Worker code and config as a new Version [beta] - wrangler versions deploy [version-specs..] Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [beta] + wrangler versions view View the details of a specific version of your Worker [open-beta] + wrangler versions list List the 10 most recent Versions of your Worker [open-beta] + wrangler versions upload Uploads your Worker code and config as a new Version [open-beta] + wrangler versions deploy [version-specs..] Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [open-beta] wrangler versions secret Generate a secret that can be referenced in a Worker GLOBAL FLAGS @@ -126,10 +126,10 @@ describe("versions subhelp", () => { 🫧 List, view, upload and deploy Versions of your Worker to Cloudflare COMMANDS - wrangler versions view View the details of a specific version of your Worker [beta] - wrangler versions list List the 10 most recent Versions of your Worker [beta] - wrangler versions upload Uploads your Worker code and config as a new Version [beta] - wrangler versions deploy [version-specs..] Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [beta] + wrangler versions view View the details of a specific version of your Worker [open-beta] + wrangler versions list List the 10 most recent Versions of your Worker [open-beta] + wrangler versions upload Uploads your Worker code and config as a new Version [open-beta] + wrangler versions deploy [version-specs..] Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [open-beta] wrangler versions secret Generate a secret that can be referenced in a Worker GLOBAL FLAGS @@ -152,10 +152,10 @@ describe("versions subhelp", () => { 🫧 List, view, upload and deploy Versions of your Worker to Cloudflare COMMANDS - wrangler versions view View the details of a specific version of your Worker [beta] - wrangler versions list List the 10 most recent Versions of your Worker [beta] - wrangler versions upload Uploads your Worker code and config as a new Version [beta] - wrangler versions deploy [version-specs..] Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [beta] + wrangler versions view View the details of a specific version of your Worker [open-beta] + wrangler versions list List the 10 most recent Versions of your Worker [open-beta] + wrangler versions upload Uploads your Worker code and config as a new Version [open-beta] + wrangler versions deploy [version-specs..] Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [open-beta] wrangler versions secret Generate a secret that can be referenced in a Worker GLOBAL FLAGS @@ -178,10 +178,10 @@ describe("versions subhelp", () => { 🫧 List, view, upload and deploy Versions of your Worker to Cloudflare COMMANDS - wrangler versions view View the details of a specific version of your Worker [beta] - wrangler versions list List the 10 most recent Versions of your Worker [beta] - wrangler versions upload Uploads your Worker code and config as a new Version [beta] - wrangler versions deploy [version-specs..] Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [beta] + wrangler versions view View the details of a specific version of your Worker [open-beta] + wrangler versions list List the 10 most recent Versions of your Worker [open-beta] + wrangler versions upload Uploads your Worker code and config as a new Version [open-beta] + wrangler versions deploy [version-specs..] Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [open-beta] wrangler versions secret Generate a secret that can be referenced in a Worker GLOBAL FLAGS @@ -204,10 +204,10 @@ describe("versions subhelp", () => { 🫧 List, view, upload and deploy Versions of your Worker to Cloudflare COMMANDS - wrangler versions view View the details of a specific version of your Worker [beta] - wrangler versions list List the 10 most recent Versions of your Worker [beta] - wrangler versions upload Uploads your Worker code and config as a new Version [beta] - wrangler versions deploy [version-specs..] Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [beta] + wrangler versions view View the details of a specific version of your Worker [open-beta] + wrangler versions list List the 10 most recent Versions of your Worker [open-beta] + wrangler versions upload Uploads your Worker code and config as a new Version [open-beta] + wrangler versions deploy [version-specs..] Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [open-beta] wrangler versions secret Generate a secret that can be referenced in a Worker GLOBAL FLAGS diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index b52860ae6c07..8c75f56a27ac 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -139,9 +139,18 @@ import { loginCommand, logoutCommand, whoamiCommand } from "./user/commands"; import { whoami } from "./user/whoami"; import { debugLogFilepath } from "./utils/log-file"; import { vectorize } from "./vectorize/index"; -import registerVersionsSubcommands from "./versions"; +import { versionsNamespace } from "./versions"; +import { versionsDeployCommand } from "./versions/deploy"; import registerVersionsDeploymentsSubcommands from "./versions/deployments"; +import { versionsListCommand } from "./versions/list"; import registerVersionsRollbackCommand from "./versions/rollback"; +import { versionsSecretNamespace } from "./versions/secrets"; +import { versionsSecretBulkCommand } from "./versions/secrets/bulk"; +import { versionsSecretDeleteCommand } from "./versions/secrets/delete"; +import { versionsSecretsListCommand } from "./versions/secrets/list"; +import { versionsSecretPutCommand } from "./versions/secrets/put"; +import { versionsUploadCommand } from "./versions/upload"; +import { versionsViewCommand } from "./versions/view"; import { workflowsInstanceNamespace, workflowsNamespace } from "./workflows"; import { workflowsDeleteCommand } from "./workflows/commands/delete"; import { workflowsDescribeCommand } from "./workflows/commands/describe"; @@ -546,13 +555,49 @@ export function createCLIParser(argv: string[]) { // versions if (experimentalGradualRollouts) { - wrangler.command( - "versions", - "🫧 List, view, upload and deploy Versions of your Worker to Cloudflare", - (yargs) => { - return registerVersionsSubcommands(yargs.command(subHelp), subHelp); - } - ); + registry.define([ + { + command: "wrangler versions", + definition: versionsNamespace, + }, + { + command: "wrangler versions view", + definition: versionsViewCommand, + }, + { + command: "wrangler versions list", + definition: versionsListCommand, + }, + { + command: "wrangler versions upload", + definition: versionsUploadCommand, + }, + { + command: "wrangler versions deploy", + definition: versionsDeployCommand, + }, + { + command: "wrangler versions secret", + definition: versionsSecretNamespace, + }, + { + command: "wrangler versions secret put", + definition: versionsSecretPutCommand, + }, + { + command: "wrangler versions secret bulk", + definition: versionsSecretBulkCommand, + }, + { + command: "wrangler versions secret delete", + definition: versionsSecretDeleteCommand, + }, + { + command: "wrangler versions secret list", + definition: versionsSecretsListCommand, + }, + ]); + registry.registerNamespace("versions"); } // triggers diff --git a/packages/wrangler/src/versions/deploy.ts b/packages/wrangler/src/versions/deploy.ts index f42b6a998616..29e80fccc341 100644 --- a/packages/wrangler/src/versions/deploy.ts +++ b/packages/wrangler/src/versions/deploy.ts @@ -1,5 +1,4 @@ import assert from "assert"; -import path from "path"; import * as cli from "@cloudflare/cli"; import { brandColor, gray, white } from "@cloudflare/cli/colors"; import { @@ -9,14 +8,13 @@ import { spinnerWhile, } from "@cloudflare/cli/interactive"; import { fetchResult } from "../cfetch"; -import { findWranglerConfig, readConfig } from "../config"; +import { createCommand } from "../core/create-command"; import { UserError } from "../errors"; import { isNonInteractiveOrCI } from "../is-interactive"; import { logger } from "../logger"; import * as metrics from "../metrics"; import { writeOutput } from "../output"; import { APIError } from "../parse"; -import { printWranglerBanner } from "../update-check"; import { requireAuth } from "../user"; import formatLabelledValues from "../utils/render-labelled-values"; import { @@ -28,10 +26,6 @@ import { patchNonVersionedScriptSettings, } from "./api"; import type { Config } from "../config"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../yargs-types"; import type { ApiDeployment, ApiVersion, @@ -44,199 +38,191 @@ const EPSILON = 0.001; // used to avoid floating-point errors. Comparions to a v const BLANK_INPUT = "-"; // To be used where optional user-input is displayed and the value is nullish const ZERO_WIDTH_SPACE = "\u200B"; // Some log lines get trimmed and so, to indent, the line is prefixed with a zero-width space -export type VersionsDeployArgs = StrictYargsOptionsToInterface< - typeof versionsDeployOptions ->; - type OptionalPercentage = number | null; // null means automatically assign (evenly distribute remaining traffic) -export function versionsDeployOptions(yargs: CommonYargsArgv) { - return yargs - .option("name", { +export const versionsDeployCommand = createCommand({ + metadata: { + description: + "Safely roll out new Versions of your Worker by splitting traffic between multiple Versions", + owner: "Workers: Authoring and Testing", + status: "open-beta", + }, + args: { + name: { describe: "Name of the worker", type: "string", requiresArg: true, - }) - .option("version-id", { + }, + "version-id": { describe: "Worker Version ID(s) to deploy", - type: "array", - string: true, + type: "string", + array: true, requiresArg: true, - }) - .option("percentage", { + }, + percentage: { describe: "Percentage of traffic to split between Worker Version(s) (0-100)", - type: "array", - number: true, + array: true, + type: "number", requiresArg: true, - }) - .positional("version-specs", { + }, + "version-specs": { describe: "Shorthand notation to deploy Worker Version(s) [@..]", type: "string", array: true, - }) - .option("message", { + }, + message: { describe: "Description of this deployment (optional)", type: "string", requiresArg: true, - }) - .option("yes", { + }, + yes: { alias: "y", describe: "Automatically accept defaults to prompts", type: "boolean", default: false, - }) - .option("dry-run", { + }, + "dry-run": { describe: "Don't actually deploy", type: "boolean", default: false, - }) - .option("max-versions", { + }, + "max-versions": { hidden: true, // experimental, not supported long-term describe: "Maximum allowed versions to select", type: "number", default: 2, // (when server-side limitation is lifted, we can update this default or just remove the option entirely) - }); -} - -export async function versionsDeployHandler(args: VersionsDeployArgs) { - await printWranglerBanner(); - - const config = getConfig(args); - await metrics.sendMetricsEvent( - "deploy worker versions", - {}, - { - sendMetrics: config.send_metrics, - } - ); - - const accountId = await requireAuth(config); - const workerName = args.name ?? config.name; - - if (workerName === undefined) { - throw new UserError( - 'You need to provide a name of your worker. Either pass it as a cli arg with `--name ` or in your config file as `name = ""`' + }, + }, + positionalArgs: ["version-specs"], + handler: async function versionsDeployHandler(args, { config }) { + await metrics.sendMetricsEvent( + "deploy worker versions", + {}, + { + sendMetrics: config.send_metrics, + } ); - } - if (config.workflows?.length) { - logger.once.warn("Workflows is currently in open beta."); - } + const accountId = await requireAuth(config); + const workerName = args.name ?? config.name; - const versionCache: VersionCache = new Map(); - const optionalVersionTraffic = parseVersionSpecs(args); + if (workerName === undefined) { + throw new UserError( + 'You need to provide a name of your worker. Either pass it as a cli arg with `--name ` or in your config file as `name = ""`' + ); + } - cli.startSection( - "Deploy Worker Versions", - "by splitting traffic between multiple versions", - true - ); + if (config.workflows?.length) { + logger.once.warn("Workflows is currently in open beta."); + } - await printLatestDeployment(accountId, workerName, versionCache); + const versionCache: VersionCache = new Map(); + const optionalVersionTraffic = parseVersionSpecs(args); - // prompt to confirm or change the versionIds from the args - const confirmedVersionsToDeploy = await promptVersionsToDeploy( - accountId, - workerName, - [...optionalVersionTraffic.keys()], - versionCache, - args.yes - ); + cli.startSection( + "Deploy Worker Versions", + "by splitting traffic between multiple versions", + true + ); - // validate we have at least 1 version - if (confirmedVersionsToDeploy.length === 0) { - throw new UserError("You must select at least 1 version to deploy."); - } + await printLatestDeployment(accountId, workerName, versionCache); - // validate we have at most experimentalMaxVersions (default: 2) - if (confirmedVersionsToDeploy.length > args.maxVersions) { - throw new UserError( - `You must select at most ${args.maxVersions} versions to deploy.` + // prompt to confirm or change the versionIds from the args + const confirmedVersionsToDeploy = await promptVersionsToDeploy( + accountId, + workerName, + [...optionalVersionTraffic.keys()], + versionCache, + args.yes ); - } - // prompt to confirm or change the percentages for each confirmed version to deploy - const confirmedVersionTraffic = await promptPercentages( - confirmedVersionsToDeploy, - optionalVersionTraffic, - args.yes - ); + // validate we have at least 1 version + if (confirmedVersionsToDeploy.length === 0) { + throw new UserError("You must select at least 1 version to deploy."); + } - // prompt for deployment message - const message = await inputPrompt({ - type: "text", - label: "Deployment message", - defaultValue: args.message, - acceptDefault: args.yes, - question: "Add a deployment message", - helpText: "(optional)", - }); + // validate we have at most experimentalMaxVersions (default: 2) + if (confirmedVersionsToDeploy.length > args.maxVersions) { + throw new UserError( + `You must select at most ${args.maxVersions} versions to deploy.` + ); + } - if (args.dryRun) { - cli.cancel("--dry-run: exiting"); - return; - } + // prompt to confirm or change the percentages for each confirmed version to deploy + const confirmedVersionTraffic = await promptPercentages( + confirmedVersionsToDeploy, + optionalVersionTraffic, + args.yes + ); - const start = Date.now(); + // prompt for deployment message + const message = await inputPrompt({ + type: "text", + label: "Deployment message", + defaultValue: args.message, + acceptDefault: args.yes, + question: "Add a deployment message", + helpText: "(optional)", + }); - const { id: deploymentId } = await spinnerWhile({ - startMessage: `Deploying ${confirmedVersionsToDeploy.length} version(s)`, - promise() { - return createDeployment( - accountId, - workerName, - confirmedVersionTraffic, - message - ); - }, - }); + if (args.dryRun) { + cli.cancel("--dry-run: exiting"); + return; + } - await maybePatchSettings(accountId, workerName, config); + const start = Date.now(); - const elapsedMilliseconds = Date.now() - start; - const elapsedSeconds = elapsedMilliseconds / 1000; - const elapsedString = `${elapsedSeconds.toFixed(2)} sec`; + const { id: deploymentId } = await spinnerWhile({ + startMessage: `Deploying ${confirmedVersionsToDeploy.length} version(s)`, + promise() { + return createDeployment( + accountId, + workerName, + confirmedVersionTraffic, + message + ); + }, + }); - const trafficSummaryList = Array.from(confirmedVersionTraffic).map( - ([versionId, percentage]) => `version ${versionId} at ${percentage}%` - ); - const trafficSummaryString = new Intl.ListFormat("en-US").format( - trafficSummaryList - ); + await maybePatchSettings(accountId, workerName, config); - cli.success( - `Deployed ${workerName} ${trafficSummaryString} (${elapsedString})` - ); + const elapsedMilliseconds = Date.now() - start; + const elapsedSeconds = elapsedMilliseconds / 1000; + const elapsedString = `${elapsedSeconds.toFixed(2)} sec`; - let workerTag: string | null = null; - try { - const serviceMetaData = await fetchResult<{ - default_environment: { script: { tag: string } }; - }>(`/accounts/${accountId}/workers/services/${workerName}`); - workerTag = serviceMetaData.default_environment.script.tag; - } catch { - // If the fetch fails then we just output a null for the workerTag. - } - writeOutput({ - type: "version-deploy", - version: 1, - worker_name: workerName, - worker_tag: workerTag, - // NOTE this deploymentId is related to the gradual rollout of the versions given in the version_traffic. - deployment_id: deploymentId, - version_traffic: confirmedVersionTraffic, - }); -} + const trafficSummaryList = Array.from(confirmedVersionTraffic).map( + ([versionId, percentage]) => `version ${versionId} at ${percentage}%` + ); + const trafficSummaryString = new Intl.ListFormat("en-US").format( + trafficSummaryList + ); -function getConfig(args: Pick) { - const configPath = - args.config || (args.name && findWranglerConfig(path.dirname(args.name))); - const config = readConfig(configPath, args); + cli.success( + `Deployed ${workerName} ${trafficSummaryString} (${elapsedString})` + ); - return config; -} + let workerTag: string | null = null; + try { + const serviceMetaData = await fetchResult<{ + default_environment: { script: { tag: string } }; + }>(`/accounts/${accountId}/workers/services/${workerName}`); + workerTag = serviceMetaData.default_environment.script.tag; + } catch { + // If the fetch fails then we just output a null for the workerTag. + } + writeOutput({ + type: "version-deploy", + version: 1, + worker_name: workerName, + worker_tag: workerTag, + // NOTE this deploymentId is related to the gradual rollout of the versions given in the version_traffic. + deployment_id: deploymentId, + version_traffic: confirmedVersionTraffic, + }); + }, +}); /** * Prompts the user for confirmation when overwriting the latest deployment, given that it's split. @@ -620,12 +606,13 @@ async function maybePatchSettings( // *********** // UNITS // *********** - +export type ParseVersionSpecsArgs = { + percentage?: number[]; + versionId?: string[]; + versionSpecs?: string[]; +}; export function parseVersionSpecs( - args: Pick< - VersionsDeployArgs, - "_" | "versionSpecs" | "versionId" | "percentage" - > + args: ParseVersionSpecsArgs ): Map { const versionIds: string[] = []; const percentages: OptionalPercentage[] = []; diff --git a/packages/wrangler/src/versions/deployments/list.ts b/packages/wrangler/src/versions/deployments/list.ts index 82e52bd7d744..f0f5500fb621 100644 --- a/packages/wrangler/src/versions/deployments/list.ts +++ b/packages/wrangler/src/versions/deployments/list.ts @@ -7,7 +7,8 @@ import { printWranglerBanner } from "../../update-check"; import { requireAuth } from "../../user"; import formatLabelledValues from "../../utils/render-labelled-values"; import { fetchLatestDeployments, fetchVersions } from "../api"; -import { getConfig, getVersionSource } from "../list"; +import { getVersionSource } from "../list"; +import { getConfig } from "../utils/config"; import type { CommonYargsArgv, StrictYargsOptionsToInterface, diff --git a/packages/wrangler/src/versions/deployments/status.ts b/packages/wrangler/src/versions/deployments/status.ts index 1b7269b6899f..ef09eb972075 100644 --- a/packages/wrangler/src/versions/deployments/status.ts +++ b/packages/wrangler/src/versions/deployments/status.ts @@ -7,7 +7,7 @@ import { printWranglerBanner } from "../../update-check"; import { requireAuth } from "../../user"; import formatLabelledValues from "../../utils/render-labelled-values"; import { fetchLatestDeployment, fetchVersions } from "../api"; -import { getConfig } from "../list"; +import { getConfig } from "../utils/config"; import { getDeploymentSource } from "./list"; import type { CommonYargsArgv, diff --git a/packages/wrangler/src/versions/index.ts b/packages/wrangler/src/versions/index.ts index 7734fa2be433..8a9e3411e2a9 100644 --- a/packages/wrangler/src/versions/index.ts +++ b/packages/wrangler/src/versions/index.ts @@ -1,354 +1,10 @@ -import assert from "node:assert"; -import path from "node:path"; -import { getAssetsOptions, validateAssetsArgsAndConfig } from "../assets"; -import { configFileName, findWranglerConfig, readConfig } from "../config"; -import { getEntry } from "../deployment-bundle/entry"; -import { UserError } from "../errors"; -import { - getRules, - getScriptName, - isLegacyEnv, - printWranglerBanner, -} from "../index"; -import { logger } from "../logger"; -import { verifyWorkerMatchesCITag } from "../match-tag"; -import * as metrics from "../metrics"; -import { writeOutput } from "../output"; -import { requireAuth } from "../user"; -import { collectKeyValues } from "../utils/collectKeyValues"; -import { versionsDeployHandler, versionsDeployOptions } from "./deploy"; -import { versionsListHandler, versionsListOptions } from "./list"; -import { registerVersionsSecretsSubcommands } from "./secrets"; -import versionsUpload from "./upload"; -import { versionsViewHandler, versionsViewOptions } from "./view"; -import type { Config } from "../config"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, - SubHelp, -} from "../yargs-types"; - -async function standardPricingWarning(config: Config) { - if (config.usage_model !== undefined) { - logger.warn( - `The \`usage_model\` defined in your ${configFileName(config.configPath)} file is deprecated and no longer used. Visit our developer docs for details: https://developers.cloudflare.com/workers/wrangler/configuration/#usage-model` - ); - } -} - -function versionsUploadOptions(yargs: CommonYargsArgv) { - return ( - yargs - .positional("script", { - describe: "The path to an entry point for your worker", - type: "string", - requiresArg: true, - }) - .option("name", { - describe: "Name of the worker", - type: "string", - requiresArg: true, - }) - // We want to have a --no-bundle flag, but yargs requires that - // we also have a --bundle flag (that it adds the --no to by itself) - // So we make a --bundle flag, but hide it, and then add a --no-bundle flag - // that's visible to the user but doesn't "do" anything. - .option("bundle", { - describe: "Run wrangler's compilation step before publishing", - type: "boolean", - hidden: true, - }) - .option("no-bundle", { - describe: "Skip internal build steps and directly deploy Worker", - type: "boolean", - default: false, - }) - .option("outdir", { - describe: "Output directory for the bundled worker", - type: "string", - requiresArg: true, - }) - .option("compatibility-date", { - describe: "Date to use for compatibility checks", - type: "string", - requiresArg: true, - }) - .option("compatibility-flags", { - describe: "Flags to use for compatibility checks", - alias: "compatibility-flag", - type: "string", - requiresArg: true, - array: true, - }) - .option("latest", { - describe: "Use the latest version of the worker runtime", - type: "boolean", - default: false, - }) - .option("assets", { - describe: "Static assets to be served. Replaces Workers Sites.", - type: "string", - requiresArg: true, - }) - .option("format", { - choices: ["modules", "service-worker"] as const, - describe: "Choose an entry type", - deprecated: true, - hidden: true, - }) - .option("legacy-assets", { - describe: "Static assets to be served", - type: "string", - requiresArg: true, - deprecated: true, - hidden: true, - }) - .option("site", { - describe: "Root folder of static assets for Workers Sites", - type: "string", - requiresArg: true, - hidden: true, - deprecated: true, - }) - .option("site-include", { - describe: - "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.", - type: "string", - requiresArg: true, - array: true, - hidden: true, - deprecated: true, - }) - .option("site-exclude", { - describe: - "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.", - type: "string", - requiresArg: true, - array: true, - hidden: true, - deprecated: true, - }) - .option("var", { - describe: - "A key-value pair to be injected into the script as a variable", - type: "string", - requiresArg: true, - array: true, - }) - .option("define", { - describe: "A key-value pair to be substituted in the script", - type: "string", - requiresArg: true, - array: true, - }) - .option("alias", { - describe: "A module pair to be substituted in the script", - type: "string", - requiresArg: true, - array: true, - }) - .option("jsx-factory", { - describe: "The function that is called for each JSX element", - type: "string", - requiresArg: true, - }) - .option("jsx-fragment", { - describe: "The function that is called for each JSX fragment", - type: "string", - requiresArg: true, - }) - .option("tsconfig", { - describe: "Path to a custom tsconfig.json file", - type: "string", - requiresArg: true, - }) - .option("minify", { - describe: "Minify the Worker", - type: "boolean", - }) - .option("upload-source-maps", { - describe: - "Include source maps when uploading this Worker Gradual Rollouts Version.", - type: "boolean", - }) - .option("node-compat", { - describe: "Enable Node.js compatibility", - type: "boolean", - }) - .option("dry-run", { - describe: "Don't actually deploy", - type: "boolean", - }) - // args only for `versions upload`, not `deploy` - .option("tag", { - describe: "A tag for this Worker Gradual Rollouts Version", - type: "string", - requiresArg: true, - }) - .option("message", { - describe: - "A descriptive message for this Worker Gradual Rollouts Version", - type: "string", - requiresArg: true, - }) - ); -} - -async function versionsUploadHandler( - args: StrictYargsOptionsToInterface -) { - await printWranglerBanner(); - - const configPath = - args.config || - (args.script && findWranglerConfig(path.dirname(args.script))); - const config = readConfig(configPath, args); - const entry = await getEntry(args, config, "versions upload"); - await metrics.sendMetricsEvent( - "upload worker version", - { - usesTypeScript: /\.tsx?$/.test(entry.file), - }, - { - sendMetrics: config.send_metrics, - } - ); - - if (args.site || config.site) { - throw new UserError( - "Workers Sites does not support uploading versions through `wrangler versions upload`. You must use `wrangler deploy` instead." - ); - } - if (args.legacyAssets || config.legacy_assets) { - throw new UserError( - "Legacy assets does not support uploading versions through `wrangler versions upload`. You must use `wrangler deploy` instead." - ); - } - - if (config.workflows?.length) { - logger.once.warn("Workflows is currently in open beta."); - } - - validateAssetsArgsAndConfig( - { - // given that legacyAssets and sites are not supported by - // `wrangler versions upload` pass them as undefined to - // skip the corresponding mutual exclusivity validation - legacyAssets: undefined, - site: undefined, - assets: args.assets, - script: args.script, - }, - config - ); - - const assetsOptions = getAssetsOptions(args, config); - - if (args.latest) { - logger.warn( - `Using the latest version of the Workers runtime. To silence this warning, please choose a specific version of the runtime with --compatibility-date, or add a compatibility_date to your ${configFileName(config.configPath)} file.\n` - ); - } - - const cliVars = collectKeyValues(args.var); - const cliDefines = collectKeyValues(args.define); - const cliAlias = collectKeyValues(args.alias); - - const accountId = args.dryRun ? undefined : await requireAuth(config); - const name = getScriptName(args, config); - - assert( - name, - 'You need to provide a name when publishing a worker. Either pass it as a cli arg with `--name ` or in your config file as `name = ""`' - ); - - if (!args.dryRun) { - assert(accountId, "Missing account ID"); - await verifyWorkerMatchesCITag( - accountId, - name, - path.relative(config.projectRoot, config.configPath ?? "wrangler.toml") - ); - } - - if (!args.dryRun) { - await standardPricingWarning(config); - } - const { versionId, workerTag, versionPreviewUrl } = await versionsUpload({ - config, - accountId, - name, - rules: getRules(config), - entry, - legacyEnv: isLegacyEnv(config), - env: args.env, - compatibilityDate: args.latest - ? new Date().toISOString().substring(0, 10) - : args.compatibilityDate, - compatibilityFlags: args.compatibilityFlags, - vars: cliVars, - defines: cliDefines, - alias: cliAlias, - jsxFactory: args.jsxFactory, - jsxFragment: args.jsxFragment, - tsconfig: args.tsconfig, - assetsOptions, - minify: args.minify, - uploadSourceMaps: args.uploadSourceMaps, - nodeCompat: args.nodeCompat, - isWorkersSite: Boolean(args.site || config.site), - outDir: args.outdir, - dryRun: args.dryRun, - noBundle: !(args.bundle ?? !config.no_bundle), - keepVars: false, - tag: args.tag, - message: args.message, - }); - - writeOutput({ - type: "version-upload", - version: 1, - worker_name: name ?? null, - worker_tag: workerTag, - version_id: versionId, - preview_url: versionPreviewUrl, - }); -} - -export default function registerVersionsSubcommands( - versionYargs: CommonYargsArgv, - subHelp: SubHelp -) { - versionYargs - .command( - "view ", - "View the details of a specific version of your Worker [beta]", - versionsViewOptions, - versionsViewHandler - ) - .command( - "list", - "List the 10 most recent Versions of your Worker [beta]", - versionsListOptions, - versionsListHandler - ) - .command( - "upload", - "Uploads your Worker code and config as a new Version [beta]", - versionsUploadOptions, - versionsUploadHandler - ) - .command( - "deploy [version-specs..]", - "Safely roll out new Versions of your Worker by splitting traffic between multiple Versions [beta]", - versionsDeployOptions, - versionsDeployHandler - ) - .command( - "secret", - "Generate a secret that can be referenced in a Worker", - (yargs) => { - return registerVersionsSecretsSubcommands(yargs.command(subHelp)); - } - ); -} +import { createNamespace } from "../core/create-command"; + +export const versionsNamespace = createNamespace({ + metadata: { + description: + "🫧 List, view, upload and deploy Versions of your Worker to Cloudflare", + status: "stable", + owner: "Workers: Authoring and Testing", + }, +}); diff --git a/packages/wrangler/src/versions/list.ts b/packages/wrangler/src/versions/list.ts index 0de47fde8cf3..4957c75dcbd1 100644 --- a/packages/wrangler/src/versions/list.ts +++ b/packages/wrangler/src/versions/list.ts @@ -1,93 +1,77 @@ -import path from "path"; import { logRaw } from "@cloudflare/cli"; -import { findWranglerConfig, readConfig } from "../config"; +import { createCommand } from "../core/create-command"; import { UserError } from "../errors"; import * as metrics from "../metrics"; -import { printWranglerBanner } from "../update-check"; import { requireAuth } from "../user"; import formatLabelledValues from "../utils/render-labelled-values"; import { fetchDeployableVersions } from "./api"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../yargs-types"; import type { ApiVersion, VersionCache } from "./types"; const BLANK_INPUT = "-"; // To be used where optional user-input is displayed and the value is nullish -export type VersionsListArgs = StrictYargsOptionsToInterface< - typeof versionsListOptions ->; - -export function versionsListOptions(yargs: CommonYargsArgv) { - return yargs - .option("name", { - describe: "Name of the worker", +export const versionsListCommand = createCommand({ + metadata: { + description: "List the 10 most recent Versions of your Worker", + owner: "Workers: Authoring and Testing", + status: "open-beta", + }, + args: { + name: { + describe: "Name of the Worker", type: "string", requiresArg: true, - }) - .option("json", { + }, + json: { describe: "Display output as clean JSON", type: "boolean", default: false, - }); -} + }, + }, + handler: async function versionsSecretListHandler(args, { config }) { + await metrics.sendMetricsEvent( + "list worker versions", + { json: args.json }, + { + sendMetrics: config.send_metrics, + } + ); -export async function versionsListHandler(args: VersionsListArgs) { - if (!args.json) { - await printWranglerBanner(); - } + const accountId = await requireAuth(config); + const workerName = args.name ?? config.name; - const config = getConfig(args); - await metrics.sendMetricsEvent( - "list worker versions", - { json: args.json }, - { - sendMetrics: config.send_metrics, + if (workerName === undefined) { + throw new UserError( + 'You need to provide a name of your worker. Either pass it as a cli arg with `--name ` or in your config file as `name = ""`' + ); } - ); - const accountId = await requireAuth(config); - const workerName = args.name ?? config.name; - - if (workerName === undefined) { - throw new UserError( - 'You need to provide a name of your worker. Either pass it as a cli arg with `--name ` or in your config file as `name = ""`' + const versionCache: VersionCache = new Map(); + const versions = ( + await fetchDeployableVersions(accountId, workerName, versionCache) + ).sort((a, b) => + a.metadata.created_on.localeCompare(b.metadata.created_on) ); - } - const versionCache: VersionCache = new Map(); - const versions = ( - await fetchDeployableVersions(accountId, workerName, versionCache) - ).sort((a, b) => a.metadata.created_on.localeCompare(b.metadata.created_on)); - - if (args.json) { - logRaw(JSON.stringify(versions, null, 2)); - return; - } - - for (const version of versions) { - const formattedVersion = formatLabelledValues({ - "Version ID": version.id, - Created: new Date(version.metadata["created_on"]).toISOString(), - Author: version.metadata.author_email, - Source: getVersionSource(version), - Tag: version.annotations?.["workers/tag"] || BLANK_INPUT, - Message: version.annotations?.["workers/message"] || BLANK_INPUT, - }); - - logRaw(formattedVersion); - logRaw(``); - } -} + if (args.json) { + logRaw(JSON.stringify(versions, null, 2)); + return; + } -export function getConfig(args: Pick) { - const configPath = - args.config || (args.name && findWranglerConfig(path.dirname(args.name))); - const config = readConfig(configPath, args); + for (const version of versions) { + const formattedVersion = formatLabelledValues({ + "Version ID": version.id, + Created: new Date(version.metadata["created_on"]).toISOString(), + Author: version.metadata.author_email, + Source: getVersionSource(version), + Tag: version.annotations?.["workers/tag"] || BLANK_INPUT, + Message: version.annotations?.["workers/message"] || BLANK_INPUT, + }); - return config; -} + logRaw(formattedVersion); + logRaw(``); + } + }, +}); export function getVersionSource(version: { metadata: Pick; diff --git a/packages/wrangler/src/versions/rollback/index.ts b/packages/wrangler/src/versions/rollback/index.ts index 705879d003d3..d7bea12981fb 100644 --- a/packages/wrangler/src/versions/rollback/index.ts +++ b/packages/wrangler/src/versions/rollback/index.ts @@ -7,7 +7,7 @@ import { APIError } from "../../parse"; import { requireAuth } from "../../user"; import { createDeployment, fetchLatestDeployments, fetchVersion } from "../api"; import { printLatestDeployment, printVersions } from "../deploy"; -import { getConfig } from "../list"; +import { getConfig } from "../utils/config"; import type { CommonYargsArgv, StrictYargsOptionsToInterface, diff --git a/packages/wrangler/src/versions/secrets/bulk.ts b/packages/wrangler/src/versions/secrets/bulk.ts index c22e52928044..fa15eb3562bb 100644 --- a/packages/wrangler/src/versions/secrets/bulk.ts +++ b/packages/wrangler/src/versions/secrets/bulk.ts @@ -1,129 +1,126 @@ import path from "node:path"; import readline from "node:readline"; import { fetchResult } from "../../cfetch"; -import { configFileName, readConfig } from "../../config"; +import { configFileName } from "../../config"; +import { createCommand } from "../../core/create-command"; import { UserError } from "../../errors"; import { getLegacyScriptName } from "../../index"; import { logger } from "../../logger"; import { parseJSON, readFileSync } from "../../parse"; import { validateJSONFileSecrets } from "../../secret"; -import { printWranglerBanner } from "../../update-check"; import { requireAuth } from "../../user"; import { copyWorkerVersionWithNewSecrets } from "./index"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; import type { WorkerVersion } from "./index"; -export function versionsSecretsPutBulkOptions(yargs: CommonYargsArgv) { - return yargs - .positional("json", { +export const versionsSecretBulkCommand = createCommand({ + metadata: { + description: "Create or update a secret variable for a Worker", + owner: "Workers: Authoring and Testing", + status: "stable", + }, + args: { + json: { describe: `The JSON file of key-value pairs to upload, in form {"key": value, ...}`, type: "string", - }) - .option("name", { + }, + name: { describe: "Name of the Worker", type: "string", requiresArg: true, - }) - .option("message", { + }, + message: { describe: "Description of this deployment", type: "string", requiresArg: true, - }) - .option("tag", { + }, + tag: { describe: "A tag for this version", type: "string", requiresArg: true, - }); -} + }, + }, + positionalArgs: ["json"], + handler: async function versionsSecretPutBulkHandler(args, { config }) { + const scriptName = getLegacyScriptName(args, config); + if (!scriptName) { + throw new UserError( + `Required Worker name missing. Please specify the Worker name in your ${configFileName(config.configPath)} file, or pass it as an argument with \`--name \`` + ); + } -export async function versionsSecretPutBulkHandler( - args: StrictYargsOptionsToInterface -) { - await printWranglerBanner(); - const config = readConfig(args.config, args, false, true); + const accountId = await requireAuth(config); - const scriptName = getLegacyScriptName(args, config); - if (!scriptName) { - throw new UserError( - `Required Worker name missing. Please specify the Worker name in your ${configFileName(config.configPath)} file, or pass it as an argument with \`--name \`` + logger.log( + `🌀 Creating the secrets for the Worker "${scriptName}" ${args.env ? `(${args.env})` : ""}` ); - } - - const accountId = await requireAuth(config); - - logger.log( - `🌀 Creating the secrets for the Worker "${scriptName}" ${args.env ? `(${args.env})` : ""}` - ); - let content: Record; - if (args.json) { - const jsonFilePath = path.resolve(args.json); - try { - content = parseJSON>( - readFileSync(jsonFilePath), - jsonFilePath - ); - } catch (e) { - return logger.error( - "Unable to parse JSON file, please ensure the file passed is valid JSON." - ); - } - validateJSONFileSecrets(content, args.json); - } else { - try { - const rl = readline.createInterface({ input: process.stdin }); - let pipedInput = ""; - for await (const line of rl) { - pipedInput += line; + let content: Record; + if (args.json) { + const jsonFilePath = path.resolve(args.json); + try { + content = parseJSON>( + readFileSync(jsonFilePath), + jsonFilePath + ); + } catch (e) { + return logger.error( + "Unable to parse JSON file, please ensure the file passed is valid JSON." + ); + } + validateJSONFileSecrets(content, args.json); + } else { + try { + const rl = readline.createInterface({ input: process.stdin }); + let pipedInput = ""; + for await (const line of rl) { + pipedInput += line; + } + content = parseJSON>(pipedInput); + } catch { + return logger.error( + "Unable to parse JSON from the input, please ensure you're passing valid JSON" + ); } - content = parseJSON>(pipedInput); - } catch { - return logger.error( - "Unable to parse JSON from the input, please ensure you're passing valid JSON" - ); } - } - if (!content) { - return logger.error(`No content found in JSON file or piped input.`); - } + if (!content) { + return logger.error(`No content found in JSON file or piped input.`); + } - const secrets = Object.entries(content).map(([key, value]) => ({ - name: key, - value, - })); + const secrets = Object.entries(content).map(([key, value]) => ({ + name: key, + value, + })); - // Grab the latest version - const versions = ( - await fetchResult<{ items: WorkerVersion[] }>( - `/accounts/${accountId}/workers/scripts/${scriptName}/versions` - ) - ).items; - if (versions.length === 0) { - throw new UserError( - "There are currently no uploaded versions of this Worker - please upload a version before uploading a secret." - ); - } - const latestVersion = versions[0]; + // Grab the latest version + const versions = ( + await fetchResult<{ items: WorkerVersion[] }>( + `/accounts/${accountId}/workers/scripts/${scriptName}/versions` + ) + ).items; + if (versions.length === 0) { + throw new UserError( + "There are currently no uploaded versions of this Worker - please upload a version before uploading a secret." + ); + } + const latestVersion = versions[0]; - const newVersion = await copyWorkerVersionWithNewSecrets({ - accountId, - scriptName, - versionId: latestVersion.id, - secrets, - versionMessage: args.message ?? `Bulk updated ${secrets.length} secrets`, - versionTag: args.tag, - sendMetrics: config.send_metrics, - }); + const newVersion = await copyWorkerVersionWithNewSecrets({ + accountId, + scriptName, + versionId: latestVersion.id, + secrets, + versionMessage: args.message ?? `Bulk updated ${secrets.length} secrets`, + versionTag: args.tag, + sendMetrics: config.send_metrics, + }); - for (const secret of secrets) { - logger.log(`✨ Successfully created secret for key: ${secret.name}`); - } - logger.log( - `✨ Success! Created version ${newVersion.id} with ${secrets.length} secrets.` + - `\n➡️ To deploy this version to production traffic use the command "wrangler versions deploy".` - ); -} + for (const secret of secrets) { + logger.log(`✨ Successfully created secret for key: ${secret.name}`); + } + logger.log( + `✨ Success! Created version ${newVersion.id} with ${secrets.length} secrets.` + + `\n➡️ To deploy this version to production traffic use the command "wrangler versions deploy".` + ); + }, +}); diff --git a/packages/wrangler/src/versions/secrets/delete.ts b/packages/wrangler/src/versions/secrets/delete.ts index 281f5ea3a732..d40734267c16 100644 --- a/packages/wrangler/src/versions/secrets/delete.ts +++ b/packages/wrangler/src/versions/secrets/delete.ts @@ -1,119 +1,118 @@ import { fetchResult } from "../../cfetch"; -import { configFileName, readConfig } from "../../config"; +import { configFileName } from "../../config"; +import { createCommand } from "../../core/create-command"; import { confirm } from "../../dialogs"; import { UserError } from "../../errors"; import { getLegacyScriptName, isLegacyEnv } from "../../index"; import { logger } from "../../logger"; -import { printWranglerBanner } from "../../update-check"; import { requireAuth } from "../../user"; import { copyWorkerVersionWithNewSecrets } from "./index"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; import type { VersionDetails, WorkerVersion } from "./index"; -export function versionsSecretsDeleteOptions(yargs: CommonYargsArgv) { - return yargs - .positional("key", { +export const versionsSecretDeleteCommand = createCommand({ + metadata: { + description: "Delete a secret variable from a Worker", + owner: "Workers: Authoring and Testing", + status: "stable", + }, + args: { + key: { describe: "The variable name to be accessible in the Worker", type: "string", - }) - .option("name", { + requiresArg: true, + }, + name: { describe: "Name of the Worker", type: "string", requiresArg: true, - }) - .option("message", { + }, + message: { describe: "Description of this deployment", type: "string", requiresArg: true, - }) - .option("tag", { + }, + tag: { describe: "A tag for this version", type: "string", requiresArg: true, - }); -} - -export async function versionsSecretDeleteHandler( - args: StrictYargsOptionsToInterface -) { - await printWranglerBanner(); - const config = readConfig(args.config, args, false, true); - - const scriptName = getLegacyScriptName(args, config); - if (!scriptName) { - throw new UserError( - `Required Worker name missing. Please specify the Worker name in your ${configFileName(config.configPath)} file, or pass it as an argument with \`--name \`` - ); - } - - if (args.key === undefined) { - throw new UserError( - "Secret name is required. Please specify the name of your secret." - ); - } - - const accountId = await requireAuth(config); - - if ( - await confirm( - `Are you sure you want to permanently delete the secret ${ - args.key - } on the Worker ${scriptName}${ - args.env && !isLegacyEnv(config) ? ` (${args.env})` : "" - }?` - ) - ) { - logger.log( - `🌀 Deleting the secret ${args.key} on the Worker ${scriptName}${ - args.env && !isLegacyEnv(config) ? ` (${args.env})` : "" - }` - ); + }, + }, + positionalArgs: ["key"], + handler: async function versionsSecretDeleteHandler(args, { config }) { + const scriptName = getLegacyScriptName(args, config); + if (!scriptName) { + throw new UserError( + `Required Worker name missing. Please specify the Worker name in your ${configFileName(config.configPath)} file, or pass it as an argument with \`--name \`` + ); + } - // Grab the latest version - const versions = ( - await fetchResult<{ items: WorkerVersion[] }>( - `/accounts/${accountId}/workers/scripts/${scriptName}/versions` - ) - ).items; - if (versions.length === 0) { + if (args.key === undefined) { throw new UserError( - "There are currently no uploaded versions of this Worker - please upload a version before uploading a secret." + "Secret name is required. Please specify the name of your secret." ); } - const latestVersion = versions[0]; - const versionInfo = await fetchResult( - `/accounts/${accountId}/workers/scripts/${scriptName}/versions/${latestVersion.id}` - ); + const accountId = await requireAuth(config); - // Go through all - const newSecrets = versionInfo.resources.bindings - .filter( - (binding) => binding.type === "secret_text" && binding.name !== args.key + if ( + await confirm( + `Are you sure you want to permanently delete the secret ${ + args.key + } on the Worker ${scriptName}${ + args.env && !isLegacyEnv(config) ? ` (${args.env})` : "" + }?` ) - .map((binding) => ({ - name: binding.name, - value: "", - inherit: true, - })); + ) { + logger.log( + `🌀 Deleting the secret ${args.key} on the Worker ${scriptName}${ + args.env && !isLegacyEnv(config) ? ` (${args.env})` : "" + }` + ); + + // Grab the latest version + const versions = ( + await fetchResult<{ items: WorkerVersion[] }>( + `/accounts/${accountId}/workers/scripts/${scriptName}/versions` + ) + ).items; + if (versions.length === 0) { + throw new UserError( + "There are currently no uploaded versions of this Worker - please upload a version before uploading a secret." + ); + } + const latestVersion = versions[0]; + + const versionInfo = await fetchResult( + `/accounts/${accountId}/workers/scripts/${scriptName}/versions/${latestVersion.id}` + ); + + // Go through all + const newSecrets = versionInfo.resources.bindings + .filter( + (binding) => + binding.type === "secret_text" && binding.name !== args.key + ) + .map((binding) => ({ + name: binding.name, + value: "", + inherit: true, + })); - const newVersion = await copyWorkerVersionWithNewSecrets({ - accountId, - scriptName, - versionId: latestVersion.id, - secrets: newSecrets, - versionMessage: args.message ?? `Deleted secret "${args.key}"`, - versionTag: args.tag, - sendMetrics: config.send_metrics, - overrideAllSecrets: true, - }); + const newVersion = await copyWorkerVersionWithNewSecrets({ + accountId, + scriptName, + versionId: latestVersion.id, + secrets: newSecrets, + versionMessage: args.message ?? `Deleted secret "${args.key}"`, + versionTag: args.tag, + sendMetrics: config.send_metrics, + overrideAllSecrets: true, + }); - logger.log( - `✨ Success! Created version ${newVersion.id} with deleted secret ${args.key}.` + - `\n➡️ To deploy this version without the secret ${args.key} to production traffic use the command "wrangler versions deploy".` - ); - } -} + logger.log( + `✨ Success! Created version ${newVersion.id} with deleted secret ${args.key}.` + + `\n➡️ To deploy this version without the secret ${args.key} to production traffic use the command "wrangler versions deploy".` + ); + } + }, +}); diff --git a/packages/wrangler/src/versions/secrets/index.ts b/packages/wrangler/src/versions/secrets/index.ts index 09530fd47b80..8c2ef4254d3a 100644 --- a/packages/wrangler/src/versions/secrets/index.ts +++ b/packages/wrangler/src/versions/secrets/index.ts @@ -1,21 +1,12 @@ import { fetchResult } from "../../cfetch"; import { performApiFetch } from "../../cfetch/internal"; +import { createNamespace } from "../../core/create-command"; import { createWorkerUploadForm, fromMimeType, } from "../../deployment-bundle/create-worker-upload-form"; import { FatalError, UserError } from "../../errors"; import { getMetricsUsageHeaders } from "../../metrics"; -import { - versionsSecretPutBulkHandler, - versionsSecretsPutBulkOptions, -} from "./bulk"; -import { - versionsSecretDeleteHandler, - versionsSecretsDeleteOptions, -} from "./delete"; -import { versionsSecretListHandler, versionsSecretsListOptions } from "./list"; -import { versionsSecretPutHandler, versionsSecretsPutOptions } from "./put"; import type { Observability } from "../../config/environment"; import type { WorkerMetadata as CfWorkerMetadata, @@ -28,36 +19,15 @@ import type { CfWorkerInit, CfWorkerSourceMap, } from "../../deployment-bundle/worker"; -import type { CommonYargsArgv } from "../../yargs-types"; import type { File, SpecIterableIterator } from "undici"; -export function registerVersionsSecretsSubcommands(yargs: CommonYargsArgv) { - return yargs - .command( - "put ", - "Create or update a secret variable for a Worker", - versionsSecretsPutOptions, - versionsSecretPutHandler - ) - .command( - "bulk [json]", - "Create or update a secret variable for a Worker", - versionsSecretsPutBulkOptions, - versionsSecretPutBulkHandler - ) - .command( - "delete ", - "Delete a secret variable from a Worker", - versionsSecretsDeleteOptions, - versionsSecretDeleteHandler - ) - .command( - "list", - "List the secrets currently deployed", - versionsSecretsListOptions, - versionsSecretListHandler - ); -} +export const versionsSecretNamespace = createNamespace({ + metadata: { + description: "Generate a secret that can be referenced in a Worker", + status: "stable", + owner: "Workers: Authoring and Testing", + }, +}); // Shared code export interface WorkerVersion { diff --git a/packages/wrangler/src/versions/secrets/list.ts b/packages/wrangler/src/versions/secrets/list.ts index bfc214e50412..13e7d9b82d35 100644 --- a/packages/wrangler/src/versions/secrets/list.ts +++ b/packages/wrangler/src/versions/secrets/list.ts @@ -1,94 +1,98 @@ import { fetchResult } from "../../cfetch"; import { configFileName, readConfig } from "../../config"; +import { createCommand } from "../../core/create-command"; import { UserError } from "../../errors"; import { getLegacyScriptName } from "../../index"; import { logger } from "../../logger"; -import { printWranglerBanner } from "../../update-check"; import { requireAuth } from "../../user"; import { fetchDeploymentVersions, fetchLatestDeployment } from "../api"; import type { VersionDetails } from "."; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; import type { ApiVersion, VersionCache } from "../types"; -export function versionsSecretsListOptions(yargs: CommonYargsArgv) { - return yargs - .option("name", { +export const versionsSecretsListCommand = createCommand({ + metadata: { + description: "List the secrets currently deployed", + owner: "Workers: Authoring and Testing", + status: "stable", + }, + args: { + name: { describe: "Name of the Worker", type: "string", requiresArg: true, - }) - .option("latest-version", { + }, + "latest-version": { describe: "Only show the latest version", type: "boolean", default: false, - }); -} + }, + }, + handler: async function versionsSecretListHandler(args) { + const config = readConfig(args.config, args, false, true); -export async function versionsSecretListHandler( - args: StrictYargsOptionsToInterface -) { - await printWranglerBanner(); - const config = readConfig(args.config, args, false, true); - - const scriptName = getLegacyScriptName(args, config); - if (!scriptName) { - throw new UserError( - `Required Worker name missing. Please specify the Worker name in your ${configFileName(config.configPath)} file, or pass it as an argument with \`--name \`` - ); - } - - const accountId = await requireAuth(config); - const versionCache: VersionCache = new Map(); - - let versions: ApiVersion[] = []; - let rollout: Map = new Map(); - if (args.latestVersion) { - // Grab the latest version - const mostRecentVersions = ( - await fetchResult<{ items: ApiVersion[] }>( - `/accounts/${accountId}/workers/scripts/${scriptName}/versions` - ) - ).items; - if (mostRecentVersions.length === 0) { + const scriptName = getLegacyScriptName(args, config); + if (!scriptName) { throw new UserError( - "There are currently no uploaded versions of this Worker - please upload a version." + `Required Worker name missing. Please specify the Worker name in your ${configFileName(config.configPath)} file, or pass it as an argument with \`--name \`` ); } - const latestVersion = mostRecentVersions[0]; - versions = [latestVersion]; - // Check if the version is in the latest deployment - const latestDeployment = await fetchLatestDeployment(accountId, scriptName); - const deploymentVersion = latestDeployment?.versions.find( - (ver) => ver.version_id === latestVersion.id - ); + const accountId = await requireAuth(config); + const versionCache: VersionCache = new Map(); - rollout.set(latestVersion.id, deploymentVersion?.percentage ?? 0); - } else { - const latestDeployment = await fetchLatestDeployment(accountId, scriptName); - [versions, rollout] = await fetchDeploymentVersions( - accountId, - scriptName, - latestDeployment, - versionCache - ); - } + let versions: ApiVersion[] = []; + let rollout: Map = new Map(); + if (args.latestVersion) { + // Grab the latest version + const mostRecentVersions = ( + await fetchResult<{ items: ApiVersion[] }>( + `/accounts/${accountId}/workers/scripts/${scriptName}/versions` + ) + ).items; + if (mostRecentVersions.length === 0) { + throw new UserError( + "There are currently no uploaded versions of this Worker - please upload a version." + ); + } + const latestVersion = mostRecentVersions[0]; + versions = [latestVersion]; - for (const version of versions) { - logger.log( - `-- Version ${version.id} (${rollout.get(version.id)}%) secrets --` - ); + // Check if the version is in the latest deployment + const latestDeployment = await fetchLatestDeployment( + accountId, + scriptName + ); + const deploymentVersion = latestDeployment?.versions.find( + (ver) => ver.version_id === latestVersion.id + ); - const secrets = (version as VersionDetails).resources.bindings.filter( - (binding) => binding.type === "secret_text" - ); - for (const secret of secrets) { - logger.log(`Secret Name: ${secret.name}`); + rollout.set(latestVersion.id, deploymentVersion?.percentage ?? 0); + } else { + const latestDeployment = await fetchLatestDeployment( + accountId, + scriptName + ); + [versions, rollout] = await fetchDeploymentVersions( + accountId, + scriptName, + latestDeployment, + versionCache + ); } - logger.log(); - } -} + for (const version of versions) { + logger.log( + `-- Version ${version.id} (${rollout.get(version.id)}%) secrets --` + ); + + const secrets = (version as VersionDetails).resources.bindings.filter( + (binding) => binding.type === "secret_text" + ); + for (const secret of secrets) { + logger.log(`Secret Name: ${secret.name}`); + } + + logger.log(); + } + }, +}); diff --git a/packages/wrangler/src/versions/secrets/put.ts b/packages/wrangler/src/versions/secrets/put.ts index 5bc8970e9093..b1b3317553c2 100644 --- a/packages/wrangler/src/versions/secrets/put.ts +++ b/packages/wrangler/src/versions/secrets/put.ts @@ -1,99 +1,97 @@ import { fetchResult } from "../../cfetch"; -import { configFileName, readConfig } from "../../config"; +import { configFileName } from "../../config"; +import { createCommand } from "../../core/create-command"; import { prompt } from "../../dialogs"; import { UserError } from "../../errors"; import { getLegacyScriptName } from "../../index"; import { logger } from "../../logger"; -import { printWranglerBanner } from "../../update-check"; import { requireAuth } from "../../user"; import { readFromStdin, trimTrailingWhitespace } from "../../utils/std"; import { copyWorkerVersionWithNewSecrets } from "./index"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; import type { WorkerVersion } from "./index"; -export function versionsSecretsPutOptions(yargs: CommonYargsArgv) { - return yargs - .positional("key", { +export const versionsSecretPutCommand = createCommand({ + metadata: { + description: "Create or update a secret variable for a Worker", + owner: "Workers: Authoring and Testing", + status: "stable", + }, + args: { + key: { describe: "The variable name to be accessible in the Worker", type: "string", - }) - .option("name", { + requiresArg: true, + }, + name: { describe: "Name of the Worker", type: "string", requiresArg: true, - }) - .option("message", { + }, + message: { describe: "Description of this deployment", type: "string", requiresArg: true, - }) - .option("tag", { + }, + tag: { describe: "A tag for this version", type: "string", requiresArg: true, - }); -} + }, + }, + positionalArgs: ["key"], + handler: async function versionsSecretPutHandler(args, { config }) { + const scriptName = getLegacyScriptName(args, config); + if (!scriptName) { + throw new UserError( + `Required Worker name missing. Please specify the Worker name in your ${configFileName(config.configPath)} file, or pass it as an argument with \`--name \`` + ); + } -export async function versionsSecretPutHandler( - args: StrictYargsOptionsToInterface -) { - await printWranglerBanner(); - const config = readConfig(args.config, args, false, true); + if (args.key === undefined) { + throw new UserError( + "Secret name is required. Please specify the name of your secret." + ); + } - const scriptName = getLegacyScriptName(args, config); - if (!scriptName) { - throw new UserError( - `Required Worker name missing. Please specify the Worker name in your ${configFileName(config.configPath)} file, or pass it as an argument with \`--name \`` - ); - } + const accountId = await requireAuth(config); - if (args.key === undefined) { - throw new UserError( - "Secret name is required. Please specify the name of your secret." + const isInteractive = process.stdin.isTTY; + const secretValue = trimTrailingWhitespace( + isInteractive + ? await prompt("Enter a secret value:", { isSecret: true }) + : await readFromStdin() ); - } - const accountId = await requireAuth(config); + logger.log( + `🌀 Creating the secret for the Worker "${scriptName}" ${args.env ? `(${args.env})` : ""}` + ); - const isInteractive = process.stdin.isTTY; - const secretValue = trimTrailingWhitespace( - isInteractive - ? await prompt("Enter a secret value:", { isSecret: true }) - : await readFromStdin() - ); + // Grab the latest version + const versions = ( + await fetchResult<{ items: WorkerVersion[] }>( + `/accounts/${accountId}/workers/scripts/${scriptName}/versions` + ) + ).items; + if (versions.length === 0) { + throw new UserError( + "There are currently no uploaded versions of this Worker. Please upload a version before uploading a secret." + ); + } + const latestVersion = versions[0]; - logger.log( - `🌀 Creating the secret for the Worker "${scriptName}" ${args.env ? `(${args.env})` : ""}` - ); + const newVersion = await copyWorkerVersionWithNewSecrets({ + accountId, + scriptName, + versionId: latestVersion.id, + secrets: [{ name: args.key, value: secretValue }], + versionMessage: args.message ?? `Updated secret "${args.key}"`, + versionTag: args.tag, + sendMetrics: config.send_metrics, + }); - // Grab the latest version - const versions = ( - await fetchResult<{ items: WorkerVersion[] }>( - `/accounts/${accountId}/workers/scripts/${scriptName}/versions` - ) - ).items; - if (versions.length === 0) { - throw new UserError( - "There are currently no uploaded versions of this Worker. Please upload a version before uploading a secret." + logger.log( + `✨ Success! Created version ${newVersion.id} with secret ${args.key}.` + + `\n➡️ To deploy this version with secret ${args.key} to production traffic use the command "wrangler versions deploy".` ); - } - const latestVersion = versions[0]; - - const newVersion = await copyWorkerVersionWithNewSecrets({ - accountId, - scriptName, - versionId: latestVersion.id, - secrets: [{ name: args.key, value: secretValue }], - versionMessage: args.message ?? `Updated secret "${args.key}"`, - versionTag: args.tag, - sendMetrics: config.send_metrics, - }); - - logger.log( - `✨ Success! Created version ${newVersion.id} with secret ${args.key}.` + - `\n➡️ To deploy this version with secret ${args.key} to production traffic use the command "wrangler versions deploy".` - ); -} + }, +}); diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index 6592898dfcc0..791b33afd875 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -1,9 +1,21 @@ +import assert from "node:assert"; import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { blue, gray } from "@cloudflare/cli/colors"; -import { syncAssets } from "../assets"; +import { + getAssetsOptions, + syncAssets, + validateAssetsArgsAndConfig, +} from "../assets"; import { fetchResult } from "../cfetch"; -import { configFileName, formatConfigSnippet, printBindings } from "../config"; +import { + configFileName, + findWranglerConfig, + formatConfigSnippet, + printBindings, + readConfig, +} from "../config"; +import { createCommand } from "../core/create-command"; import { getBindings } from "../deployment-bundle/bindings"; import { bundleWorker } from "../deployment-bundle/bundle"; import { @@ -12,6 +24,7 @@ import { } from "../deployment-bundle/bundle-reporter"; import { getBundleType } from "../deployment-bundle/bundle-type"; import { createWorkerUploadForm } from "../deployment-bundle/create-worker-upload-form"; +import { getEntry } from "../deployment-bundle/entry"; import { logBuildOutput } from "../deployment-bundle/esbuild-plugins/log-build-output"; import { findAdditionalModules, @@ -26,9 +39,13 @@ import { loadSourceMaps } from "../deployment-bundle/source-maps"; import { confirm } from "../dialogs"; import { getMigrationsToUpload } from "../durable"; import { UserError } from "../errors"; +import { getRules, getScriptName, isLegacyEnv } from "../index"; import { logger } from "../logger"; +import { verifyWorkerMatchesCITag } from "../match-tag"; import { getMetricsUsageHeaders } from "../metrics"; +import * as metrics from "../metrics"; import { isNavigatorDefined } from "../navigator-user-agent"; +import { writeOutput } from "../output"; import { ParseError } from "../parse"; import { getWranglerTmpDir } from "../paths"; import { ensureQueuesExistByConfig } from "../queues/client"; @@ -37,6 +54,8 @@ import { getSourceMappedString, maybeRetrieveFileSourceMap, } from "../sourcemap"; +import { requireAuth } from "../user"; +import { collectKeyValues } from "../utils/collectKeyValues"; import { retryOnError } from "../utils/retry"; import type { AssetsOptions } from "../assets"; import type { Config } from "../config"; @@ -112,6 +131,289 @@ function errIsStartupErr(err: unknown): err is ParseError & { code: 10021 } { return false; } +export const versionsUploadCommand = createCommand({ + metadata: { + description: "Uploads your Worker code and config as a new Version", + owner: "Workers: Authoring and Testing", + status: "open-beta", + }, + args: { + script: { + describe: "The path to an entry point for your Worker", + type: "string", + requiresArg: true, + }, + name: { + describe: "Name of the worker", + type: "string", + requiresArg: true, + }, + bundle: { + describe: "Run wrangler's compilation step before publishing", + type: "boolean", + hidden: true, + }, + "no-bundle": { + describe: "Skip internal build steps and directly deploy Worker", + type: "boolean", + default: false, + }, + outdir: { + describe: "Output directory for the bundled Worker", + type: "string", + requiresArg: true, + }, + "compatibility-date": { + describe: "Date to use for compatibility checks", + type: "string", + requiresArg: true, + }, + "compatibility-flags": { + describe: "Flags to use for compatibility checks", + alias: "compatibility-flag", + type: "string", + requiresArg: true, + array: true, + }, + latest: { + describe: "Use the latest version of the Worker runtime", + type: "boolean", + default: false, + }, + assets: { + describe: "Static assets to be served. Replaces Workers Sites.", + type: "string", + requiresArg: true, + }, + format: { + choices: ["modules", "service-worker"] as const, + describe: "Choose an entry type", + deprecated: true, + hidden: true, + }, + "legacy-assets": { + describe: "Static assets to be served", + type: "string", + requiresArg: true, + deprecated: true, + hidden: true, + }, + site: { + describe: "Root folder of static assets for Workers Sites", + type: "string", + requiresArg: true, + hidden: true, + deprecated: true, + }, + "site-include": { + describe: + "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.", + type: "string", + requiresArg: true, + array: true, + hidden: true, + deprecated: true, + }, + "site-exclude": { + describe: + "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.", + type: "string", + requiresArg: true, + array: true, + hidden: true, + deprecated: true, + }, + var: { + describe: "A key-value pair to be injected into the script as a variable", + type: "string", + requiresArg: true, + array: true, + }, + define: { + describe: "A key-value pair to be substituted in the script", + type: "string", + requiresArg: true, + array: true, + }, + alias: { + describe: "A module pair to be substituted in the script", + type: "string", + requiresArg: true, + array: true, + }, + "jsx-factory": { + describe: "The function that is called for each JSX element", + type: "string", + requiresArg: true, + }, + "jsx-fragment": { + describe: "The function that is called for each JSX fragment", + type: "string", + requiresArg: true, + }, + tsconfig: { + describe: "Path to a custom tsconfig.json file", + type: "string", + requiresArg: true, + }, + minify: { + describe: "Minify the Worker", + type: "boolean", + }, + "upload-source-maps": { + describe: + "Include source maps when uploading this Worker Gradual Rollouts Version.", + type: "boolean", + }, + "node-compat": { + describe: "Enable Node.js compatibility", + type: "boolean", + }, + "dry-run": { + describe: "Don't actually deploy", + type: "boolean", + }, + tag: { + describe: "A tag for this Worker Gradual Rollouts Version", + type: "string", + requiresArg: true, + }, + message: { + describe: + "A descriptive message for this Worker Gradual Rollouts Version", + type: "string", + requiresArg: true, + }, + }, + behaviour: { + provideConfig: false, + }, + handler: async function versionsUploadHandler(args) { + const configPath = + args.config || + (args.script && findWranglerConfig(path.dirname(args.script))); + const config = readConfig(configPath, args); + const entry = await getEntry(args, config, "versions upload"); + await metrics.sendMetricsEvent( + "upload worker version", + { + usesTypeScript: /\.tsx?$/.test(entry.file), + }, + { + sendMetrics: config.send_metrics, + } + ); + + if (args.site || config.site) { + throw new UserError( + "Workers Sites does not support uploading versions through `wrangler versions upload`. You must use `wrangler deploy` instead." + ); + } + if (args.legacyAssets || config.legacy_assets) { + throw new UserError( + "Legacy assets does not support uploading versions through `wrangler versions upload`. You must use `wrangler deploy` instead." + ); + } + + if (config.workflows?.length) { + logger.once.warn("Workflows is currently in open beta."); + } + + validateAssetsArgsAndConfig( + { + // given that legacyAssets and sites are not supported by + // `wrangler versions upload` pass them as undefined to + // skip the corresponding mutual exclusivity validation + legacyAssets: undefined, + site: undefined, + assets: args.assets, + script: args.script, + }, + config + ); + + const assetsOptions = getAssetsOptions(args, config); + + if (args.latest) { + logger.warn( + `Using the latest version of the Workers runtime. To silence this warning, please choose a specific version of the runtime with --compatibility-date, or add a compatibility_date to your ${configFileName(config.configPath)} file.\n` + ); + } + + const cliVars = collectKeyValues(args.var); + const cliDefines = collectKeyValues(args.define); + const cliAlias = collectKeyValues(args.alias); + + const accountId = args.dryRun ? undefined : await requireAuth(config); + const name = getScriptName(args, config); + + assert( + name, + 'You need to provide a name when publishing a worker. Either pass it as a cli arg with `--name ` or in your config file as `name = ""`' + ); + + if (!args.dryRun) { + assert(accountId, "Missing account ID"); + await verifyWorkerMatchesCITag( + accountId, + name, + path.relative(config.projectRoot, config.configPath ?? "wrangler.toml") + ); + } + + if (!args.dryRun) { + await standardPricingWarning(config); + } + const { versionId, workerTag, versionPreviewUrl } = await versionsUpload({ + config, + accountId, + name, + rules: getRules(config), + entry, + legacyEnv: isLegacyEnv(config), + env: args.env, + compatibilityDate: args.latest + ? new Date().toISOString().substring(0, 10) + : args.compatibilityDate, + compatibilityFlags: args.compatibilityFlags, + vars: cliVars, + defines: cliDefines, + alias: cliAlias, + jsxFactory: args.jsxFactory, + jsxFragment: args.jsxFragment, + tsconfig: args.tsconfig, + assetsOptions, + minify: args.minify, + uploadSourceMaps: args.uploadSourceMaps, + nodeCompat: args.nodeCompat, + isWorkersSite: Boolean(args.site || config.site), + outDir: args.outdir, + dryRun: args.dryRun, + noBundle: !(args.bundle ?? !config.no_bundle), + keepVars: false, + tag: args.tag, + message: args.message, + }); + + writeOutput({ + type: "version-upload", + version: 1, + worker_name: name ?? null, + worker_tag: workerTag, + version_id: versionId, + preview_url: versionPreviewUrl, + }); + }, +}); + +async function standardPricingWarning(config: Config) { + if (config.usage_model !== undefined) { + logger.warn( + `The \`usage_model\` defined in your ${configFileName(config.configPath)} file is deprecated and no longer used. Visit our developer docs for details: https://developers.cloudflare.com/workers/wrangler/configuration/#usage-model` + ); + } +} + export default async function versionsUpload(props: Props): Promise<{ versionId: string | null; workerTag: string | null; diff --git a/packages/wrangler/src/versions/utils/config.ts b/packages/wrangler/src/versions/utils/config.ts new file mode 100644 index 000000000000..e46b634aa149 --- /dev/null +++ b/packages/wrangler/src/versions/utils/config.ts @@ -0,0 +1,13 @@ +import path from "path"; +import { findWranglerConfig, readConfig } from "../../config"; + +export function getConfig< + T extends { + name?: string; + config?: string; + }, +>(args: Pick) { + const configPath = + args.config || (args.name && findWranglerConfig(path.dirname(args.name))); + return readConfig(configPath, args); +} diff --git a/packages/wrangler/src/versions/view.ts b/packages/wrangler/src/versions/view.ts index 0f84fddf6085..16a219885624 100644 --- a/packages/wrangler/src/versions/view.ts +++ b/packages/wrangler/src/versions/view.ts @@ -1,141 +1,141 @@ import { logRaw } from "@cloudflare/cli"; +import { createCommand } from "../core/create-command"; import { UserError } from "../errors"; import * as metrics from "../metrics"; import { printWranglerBanner } from "../update-check"; import { requireAuth } from "../user"; import formatLabelledValues from "../utils/render-labelled-values"; import { fetchVersion } from "./api"; -import { getConfig, getVersionSource } from "./list"; +import { getVersionSource } from "./list"; import type { WorkerMetadataBinding } from "../deployment-bundle/create-worker-upload-form"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../yargs-types"; const BLANK_INPUT = "-"; // To be used where optional user-input is displayed and the value is nullish -export type VersionsViewArgs = StrictYargsOptionsToInterface< - typeof versionsViewOptions ->; - -export function versionsViewOptions(yargs: CommonYargsArgv) { - return yargs - .positional("version-id", { +export const versionsViewCommand = createCommand({ + metadata: { + description: "View the details of a specific version of your Worker", + owner: "Workers: Authoring and Testing", + status: "open-beta", + }, + args: { + "version-id": { describe: "The Worker Version ID to view", type: "string", requiresArg: true, demandOption: true, - }) - .option("name", { + }, + name: { describe: "Name of the worker", type: "string", requiresArg: true, - }) - .option("json", { + }, + json: { describe: "Display output as clean JSON", type: "boolean", default: false, - }); -} - -export async function versionsViewHandler(args: VersionsViewArgs) { - if (!args.json) { - await printWranglerBanner(); - } - - const config = getConfig(args); - await metrics.sendMetricsEvent( - "view worker version", - {}, - { - sendMetrics: config.send_metrics, + }, + }, + positionalArgs: ["version-id"], + handler: async function versionsViewHandler(args, { config }) { + if (!args.json) { + await printWranglerBanner(); } - ); - - const accountId = await requireAuth(config); - const workerName = args.name ?? config.name; - if (workerName === undefined) { - throw new UserError( - 'You need to provide a name of your worker. Either pass it as a cli arg with `--name ` or in your config file as `name = ""`' + await metrics.sendMetricsEvent( + "view worker version", + {}, + { + sendMetrics: config.send_metrics, + } ); - } - const version = await fetchVersion(accountId, workerName, args.versionId); + const accountId = await requireAuth(config); + const workerName = args.name ?? config.name; - if (args.json) { - logRaw(JSON.stringify(version, null, 2)); - return; - } + if (workerName === undefined) { + throw new UserError( + 'You need to provide a name of your worker. Either pass it as a cli arg with `--name ` or in your config file as `name = ""`' + ); + } - logRaw( - formatLabelledValues({ - "Version ID": version.id, - Created: new Date(version.metadata["created_on"]).toISOString(), - Author: version.metadata.author_email, - Source: getVersionSource(version), - Tag: version.annotations?.["workers/tag"] || BLANK_INPUT, - Message: version.annotations?.["workers/message"] || BLANK_INPUT, - }) - ); - logRaw("------------------------------------------------------------"); - const scriptInfo: ScriptInfoLog = { - Handlers: version.resources.script.handlers.join(", "), - }; - if (version.resources.script_runtime.compatibility_date) { - scriptInfo["Compatibility Date"] = - version.resources.script_runtime.compatibility_date; - } - if (version.resources.script_runtime.compatibility_flags) { - scriptInfo["Compatibility Flags"] = - version.resources.script_runtime.compatibility_flags.join(", "); - } - logRaw(formatLabelledValues(scriptInfo)); + const version = await fetchVersion(accountId, workerName, args.versionId); - const secrets = version.resources.bindings.filter( - (binding) => binding.type === "secret_text" - ); - if (secrets.length > 0) { - logRaw("------------------------- secrets -------------------------"); - for (const secret of secrets) { - logRaw( - formatLabelledValues({ - "Secret Name": secret.name, - }) - ); + if (args.json) { + logRaw(JSON.stringify(version, null, 2)); + return; } - } - const bindings = version.resources.bindings.filter( - (binding) => binding.type !== "secret_text" - ); - if (bindings.length > 0) { - logRaw("------------------------- bindings -------------------------"); - // env vars are done differently so target them first - const envVars = bindings.filter((binding) => binding.type === "plain_text"); - if (envVars.length > 0) { - logRaw( - `[vars]\n` + - // ts is having issues typing from the filter - (envVars as { type: "plain_text"; name: string; text: string }[]) - .map((envVar) => `${envVar.name} = "${envVar.text}"`) - .join("\n") - ); + logRaw( + formatLabelledValues({ + "Version ID": version.id, + Created: new Date(version.metadata["created_on"]).toISOString(), + Author: version.metadata.author_email, + Source: getVersionSource(version), + Tag: version.annotations?.["workers/tag"] || BLANK_INPUT, + Message: version.annotations?.["workers/message"] || BLANK_INPUT, + }) + ); + logRaw("------------------------------------------------------------"); + const scriptInfo: ScriptInfoLog = { + Handlers: version.resources.script.handlers.join(", "), + }; + if (version.resources.script_runtime.compatibility_date) { + scriptInfo["Compatibility Date"] = + version.resources.script_runtime.compatibility_date; + } + if (version.resources.script_runtime.compatibility_flags) { + scriptInfo["Compatibility Flags"] = + version.resources.script_runtime.compatibility_flags.join(", "); } + logRaw(formatLabelledValues(scriptInfo)); - // Filter out env vars since they got handled above - const restOfBindings = bindings.filter( - (binding) => binding.type !== "plain_text" + const secrets = version.resources.bindings.filter( + (binding) => binding.type === "secret_text" ); - for (const binding of restOfBindings) { - const output = printBindingAsToml(binding); - if (output !== null) { - logRaw(output); - logRaw(""); + if (secrets.length > 0) { + logRaw("------------------------- secrets -------------------------"); + for (const secret of secrets) { + logRaw( + formatLabelledValues({ + "Secret Name": secret.name, + }) + ); } } - } -} + + const bindings = version.resources.bindings.filter( + (binding) => binding.type !== "secret_text" + ); + if (bindings.length > 0) { + logRaw("------------------------- bindings -------------------------"); + // env vars are done differently so target them first + const envVars = bindings.filter( + (binding) => binding.type === "plain_text" + ); + if (envVars.length > 0) { + logRaw( + `[vars]\n` + + // ts is having issues typing from the filter + (envVars as { type: "plain_text"; name: string; text: string }[]) + .map((envVar) => `${envVar.name} = "${envVar.text}"`) + .join("\n") + ); + } + + // Filter out env vars since they got handled above + const restOfBindings = bindings.filter( + (binding) => binding.type !== "plain_text" + ); + for (const binding of restOfBindings) { + const output = printBindingAsToml(binding); + if (output !== null) { + logRaw(output); + logRaw(""); + } + } + } + }, +}); type ScriptInfoLog = { Handlers: string;