diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index 53c9cfb2dcbe..92a085a99524 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -47,6 +47,21 @@ import { initHandler, initOptions } from "./init"; import "./docs"; import "./dev"; import "./kv"; +import "./versions"; +import "./versions/deploy"; +import "./versions/list"; +import "./versions/upload"; +import "./versions/view"; +import "./versions/deployments"; +import "./versions/deployments/list"; +import "./versions/deployments/status"; +import "./versions/deployments/view"; +import "./versions/rollback"; +import "./versions/secrets"; +import "./versions/secrets/bulk"; +import "./versions/secrets/delete"; +import "./versions/secrets/list"; +import "./versions/secrets/put"; import "./workflows"; import "./user/commands"; import { demandSingleValue } from "./core"; @@ -74,9 +89,6 @@ import { getAuthFromEnv } from "./user"; import { whoami } from "./user/whoami"; import { debugLogFilepath } from "./utils/log-file"; import { vectorize } from "./vectorize/index"; -import registerVersionsSubcommands from "./versions"; -import registerVersionsDeploymentsSubcommands from "./versions/deployments"; -import registerVersionsRollbackCommand from "./versions/rollback"; import { asJson } from "./yargs-types"; import type { Config } from "./config"; import type { LoggerLevel } from "./logger"; @@ -344,11 +356,7 @@ export function createCLIParser(argv: string[]) { "🚢 List and view the current and past deployments for your Worker"; if (experimentalGradualRollouts) { - wrangler.command( - "deployments", - deploymentsDescription, - registerVersionsDeploymentsSubcommands - ); + register.registerNamespace("deployments"); } else { wrangler.command("deployments", deploymentsDescription, (yargs) => yargs @@ -395,7 +403,7 @@ export function createCLIParser(argv: string[]) { const rollbackDescription = "🔙 Rollback a deployment for a Worker"; if (experimentalGradualRollouts) { - registerVersionsRollbackCommand(wrangler, rollbackDescription); + register.registerNamespace("rollback"); } else { wrangler.command( "rollback [deployment-id]", @@ -433,15 +441,9 @@ export function createCLIParser(argv: string[]) { ); } - // versions + // versions/versions deployments/versions rollback/versions secrets if (experimentalGradualRollouts) { - wrangler.command( - "versions", - "🫧 List, view, upload and deploy Versions of your Worker to Cloudflare", - (yargs) => { - return registerVersionsSubcommands(yargs.command(subHelp), subHelp); - } - ); + register.registerNamespace("versions"); } // triggers diff --git a/packages/wrangler/src/versions/deploy.ts b/packages/wrangler/src/versions/deploy.ts index ba35ba166e1d..3b1cb3f066f3 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,7 +8,7 @@ import { spinnerWhile, } from "@cloudflare/cli/interactive"; import { fetchResult } from "../cfetch"; -import { findWranglerToml, readConfig } from "../config"; +import { defineCommand } from "../core"; import { UserError } from "../errors"; import { isNonInteractiveOrCI } from "../is-interactive"; import { logger } from "../logger"; @@ -28,10 +27,6 @@ import { patchNonVersionedScriptSettings, } from "./api"; import type { Config } from "../config"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../yargs-types"; import type { ApiDeployment, ApiVersion, @@ -44,201 +39,193 @@ 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", { +defineCommand({ + command: "wrangler versions deploy", + 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 = ""`' + }, + }, + handler: async function versionsDeployHandler(args, { config }) { + await printWranglerBanner(); + + 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 && findWranglerToml(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. @@ -623,12 +610,11 @@ async function maybePatchSettings( // UNITS // *********** -export function parseVersionSpecs( - args: Pick< - VersionsDeployArgs, - "_" | "versionSpecs" | "versionId" | "percentage" - > -): Map { +export function parseVersionSpecs(args: { + percentage?: number[]; + versionId?: string[]; + versionSpecs?: string[]; +}): Map { const versionIds: string[] = []; const percentages: OptionalPercentage[] = []; @@ -658,7 +644,7 @@ export function parseVersionSpecs( percentages.push(percentage); } - // after parsing positonal args, merge in the explicit args + // after parsing positional args, merge in the explicit args // the 2 kinds of args shouldn't be used together but, if they are, positional args are given precedence const UUID_REGEX = diff --git a/packages/wrangler/src/versions/deployments/index.ts b/packages/wrangler/src/versions/deployments/index.ts index 1707f444e40d..c8a60f726027 100644 --- a/packages/wrangler/src/versions/deployments/index.ts +++ b/packages/wrangler/src/versions/deployments/index.ts @@ -1,37 +1,11 @@ -import { - versionsDeploymentsListHandler, - versionsDeploymentsListOptions, -} from "./list"; -import { - versionsDeploymentsStatusHandler, - versionsDeploymentsStatusOptions, -} from "./status"; -import { - versionsDeploymentsViewHandler, - versionsDeploymentsViewOptions, -} from "./view"; -import type { CommonYargsArgv } from "../../yargs-types"; +import { defineNamespace } from "../../core"; -export default function registerVersionsDeploymentsSubcommands( - versionDeploymentsYargs: CommonYargsArgv -) { - versionDeploymentsYargs - .command( - "list", - "Displays the 10 most recent deployments of your Worker", - versionsDeploymentsListOptions, - versionsDeploymentsListHandler - ) - .command( - "status", - "View the current state of your production", - versionsDeploymentsStatusOptions, - versionsDeploymentsStatusHandler - ) - .command( - "view [deployment-id]", - false, - versionsDeploymentsViewOptions, - versionsDeploymentsViewHandler - ); -} +defineNamespace({ + command: "wrangler deployments", + metadata: { + description: + "🚢 List and view the current and past deployments for your Worker", + status: "open-beta", + owner: "Workers: Authoring and Testing", + }, +}); diff --git a/packages/wrangler/src/versions/deployments/list.ts b/packages/wrangler/src/versions/deployments/list.ts index 82e52bd7d744..c1152de72f94 100644 --- a/packages/wrangler/src/versions/deployments/list.ts +++ b/packages/wrangler/src/versions/deployments/list.ts @@ -1,118 +1,110 @@ import assert from "assert"; import { logRaw } from "@cloudflare/cli"; import { brandColor, gray } from "@cloudflare/cli/colors"; +import { defineCommand } from "../../core"; 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 { fetchLatestDeployments, fetchVersions } from "../api"; -import { getConfig, getVersionSource } from "../list"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; -import type { ApiDeployment, VersionCache } from "../types"; +import { getDeploymentSource } from "./utils"; +import type { VersionCache } from "../types"; const BLANK_INPUT = "-"; // To be used where optional user-input is displayed and the value is nullish -export type VersionsDeloymentsListArgs = StrictYargsOptionsToInterface< - typeof versionsDeploymentsListOptions ->; - -export function versionsDeploymentsListOptions(yargs: CommonYargsArgv) { - return yargs - .option("name", { +defineCommand({ + command: "wrangler versions deploy list", + metadata: { + description: "Displays the 10 most recent deployments 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, - }); -} - -export async function versionsDeploymentsListHandler( - args: VersionsDeloymentsListArgs -) { - if (!args.json) { - await printWranglerBanner(); - } - - const config = getConfig(args); - await metrics.sendMetricsEvent( - "list versioned deployments", - { json: args.json }, - { - sendMetrics: config.send_metrics, + }, + }, + behaviour: { + printBanner: false, + }, + handler: async function versionsDeploymentsListHandler(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( + "list versioned deployments", + { json: args.json }, + { + sendMetrics: config.send_metrics, + } ); - } - - const deployments = ( - await fetchLatestDeployments(accountId, workerName) - ).sort((a, b) => a.created_on.localeCompare(b.created_on)); - - if (args.json) { - logRaw(JSON.stringify(deployments, null, 2)); - return; - } - const versionCache: VersionCache = new Map(); - const versionIds = deployments.flatMap((d) => - d.versions.map((v) => v.version_id) - ); - await fetchVersions(accountId, workerName, versionCache, ...versionIds); + const accountId = await requireAuth(config); + const workerName = args.name ?? config.name; - const formattedDeployments = deployments.map((deployment) => { - const formattedVersions = deployment.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(), - Tag: version.annotations?.["workers/tag"] || BLANK_INPUT, - Message: version.annotations?.["workers/message"] || BLANK_INPUT, - }, - { - indentationCount: 4, - labelJustification: "right", - formatLabel: (label) => gray(label + ":"), - formatValue: (value) => gray(value), - } + 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 = ""`' ); + } - return `${percentage} ${version.id}\n${details}`; - }); + const deployments = ( + await fetchLatestDeployments(accountId, workerName) + ).sort((a, b) => a.created_on.localeCompare(b.created_on)); - return formatLabelledValues({ - // explicitly not outputting Deployment ID - Created: new Date(deployment.created_on).toISOString(), - Author: deployment.author_email, - Source: getDeploymentSource(deployment), - Message: deployment.annotations?.["workers/message"] || BLANK_INPUT, - "Version(s)": formattedVersions.join("\n\n"), - }); - }); + if (args.json) { + logRaw(JSON.stringify(deployments, null, 2)); + return; + } - logRaw(formattedDeployments.join("\n\n")); -} + const versionCache: VersionCache = new Map(); + const versionIds = deployments.flatMap((d) => + d.versions.map((v) => v.version_id) + ); + await fetchVersions(accountId, workerName, versionCache, ...versionIds); + + const formattedDeployments = deployments.map((deployment) => { + const formattedVersions = deployment.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(), + Tag: version.annotations?.["workers/tag"] || BLANK_INPUT, + Message: version.annotations?.["workers/message"] || BLANK_INPUT, + }, + { + indentationCount: 4, + labelJustification: "right", + formatLabel: (label) => gray(label + ":"), + formatValue: (value) => gray(value), + } + ); + + return `${percentage} ${version.id}\n${details}`; + }); + + return formatLabelledValues({ + // explicitly not outputting Deployment ID + Created: new Date(deployment.created_on).toISOString(), + Author: deployment.author_email, + Source: getDeploymentSource(deployment), + Message: deployment.annotations?.["workers/message"] || BLANK_INPUT, + "Version(s)": formattedVersions.join("\n\n"), + }); + }); -export function getDeploymentSource(deployment: ApiDeployment) { - return getVersionSource({ - metadata: { source: deployment.source }, - annotations: deployment.annotations, - }); -} + logRaw(formattedDeployments.join("\n\n")); + }, +}); diff --git a/packages/wrangler/src/versions/deployments/status.ts b/packages/wrangler/src/versions/deployments/status.ts index 1b7269b6899f..6a8b12e0faf7 100644 --- a/packages/wrangler/src/versions/deployments/status.ts +++ b/packages/wrangler/src/versions/deployments/status.ts @@ -1,110 +1,108 @@ import assert from "assert"; import { logRaw } from "@cloudflare/cli"; import { brandColor, gray } from "@cloudflare/cli/colors"; +import { defineCommand } from "../../core"; 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 { fetchLatestDeployment, fetchVersions } from "../api"; -import { getConfig } from "../list"; -import { getDeploymentSource } from "./list"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; +import { getDeploymentSource } from "./utils"; import type { VersionCache } from "../types"; const BLANK_INPUT = "-"; // To be used where optional user-input is displayed and the value is nullish -export type VersionsDeploymentsStatusArgs = StrictYargsOptionsToInterface< - typeof versionsDeploymentsStatusOptions ->; - -export function versionsDeploymentsStatusOptions(yargs: CommonYargsArgv) { - return yargs - .option("name", { +defineCommand({ + command: "wrangler versions deploy status", + metadata: { + description: "View the current state of your production", + 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, - }); -} - -export async function versionsDeploymentsStatusHandler( - args: VersionsDeploymentsStatusArgs -) { - if (!args.json) { - await printWranglerBanner(); - } - - const config = getConfig(args); - await metrics.sendMetricsEvent( - "view latest versioned deployment", - {}, - { - sendMetrics: config.send_metrics, + }, + }, + behaviour: { + printBanner: false, + }, + handler: async function versionsDeploymentsStatusHandler(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 latest versioned deployment", + {}, + { + sendMetrics: config.send_metrics, + } ); - } - const latestDeployment = await fetchLatestDeployment(accountId, workerName); + const accountId = await requireAuth(config); + const workerName = args.name ?? config.name; - if (!latestDeployment) { - throw new UserError(`The worker ${workerName} has no deployments.`); - } - - if (args.json) { - logRaw(JSON.stringify(latestDeployment, 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 = ""`' + ); + } - const versionCache: VersionCache = new Map(); - const versionIds = latestDeployment.versions.map((v) => v.version_id); - await fetchVersions(accountId, workerName, versionCache, ...versionIds); + const latestDeployment = await fetchLatestDeployment(accountId, workerName); - const formattedVersions = latestDeployment.versions.map((traffic) => { - const version = versionCache.get(traffic.version_id); - assert(version); + if (!latestDeployment) { + throw new UserError(`The worker ${workerName} has no deployments.`); + } - const percentage = brandColor(`(${traffic.percentage}%)`); - const details = formatLabelledValues( - { - Created: new Date(version.metadata["created_on"]).toISOString(), - Tag: version.annotations?.["workers/tag"] || BLANK_INPUT, - Message: version.annotations?.["workers/message"] || BLANK_INPUT, - }, - { - indentationCount: 4, - labelJustification: "right", - formatLabel: (label) => gray(label + ":"), - formatValue: (value) => gray(value), - } - ); + if (args.json) { + logRaw(JSON.stringify(latestDeployment, null, 2)); + return; + } - return `${percentage} ${version.id}\n${details}`; - }); + const versionCache: VersionCache = new Map(); + const versionIds = latestDeployment.versions.map((v) => v.version_id); + await fetchVersions(accountId, workerName, versionCache, ...versionIds); + + const formattedVersions = latestDeployment.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(), + Tag: version.annotations?.["workers/tag"] || BLANK_INPUT, + Message: version.annotations?.["workers/message"] || BLANK_INPUT, + }, + { + indentationCount: 4, + labelJustification: "right", + formatLabel: (label) => gray(label + ":"), + formatValue: (value) => gray(value), + } + ); + + return `${percentage} ${version.id}\n${details}`; + }); - const formattedDeployment = formatLabelledValues({ - // explicitly not outputting Deployment ID - Created: new Date(latestDeployment.created_on).toISOString(), - Author: latestDeployment.author_email, - Source: getDeploymentSource(latestDeployment), - Message: latestDeployment.annotations?.["workers/message"] || BLANK_INPUT, - "Version(s)": formattedVersions.join("\n\n"), - }); + const formattedDeployment = formatLabelledValues({ + // explicitly not outputting Deployment ID + Created: new Date(latestDeployment.created_on).toISOString(), + Author: latestDeployment.author_email, + Source: getDeploymentSource(latestDeployment), + Message: latestDeployment.annotations?.["workers/message"] || BLANK_INPUT, + "Version(s)": formattedVersions.join("\n\n"), + }); - logRaw(formattedDeployment); -} + logRaw(formattedDeployment); + }, +}); diff --git a/packages/wrangler/src/versions/deployments/utils.ts b/packages/wrangler/src/versions/deployments/utils.ts new file mode 100644 index 000000000000..f2f2fe8706a3 --- /dev/null +++ b/packages/wrangler/src/versions/deployments/utils.ts @@ -0,0 +1,9 @@ +import { getVersionSource } from "../list"; +import type { ApiDeployment } from "../types"; + +export function getDeploymentSource(deployment: ApiDeployment) { + return getVersionSource({ + metadata: { source: deployment.source }, + annotations: deployment.annotations, + }); +} diff --git a/packages/wrangler/src/versions/deployments/view.ts b/packages/wrangler/src/versions/deployments/view.ts index ddd3c4162d58..3ae2ed227439 100644 --- a/packages/wrangler/src/versions/deployments/view.ts +++ b/packages/wrangler/src/versions/deployments/view.ts @@ -1,41 +1,36 @@ +import { defineCommand } from "../../core"; import { UserError } from "../../errors"; -import { printWranglerBanner } from "../../update-check"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; -export type VersionsDeploymentsViewArgs = StrictYargsOptionsToInterface< - typeof versionsDeploymentsViewOptions ->; - -export function versionsDeploymentsViewOptions(yargs: CommonYargsArgv) { - return yargs - .option("name", { +defineCommand({ + command: "wrangler versions deploy view", + metadata: { + description: + "`wrangler deployments view` has been renamed to `wrangler deployments status`. Please use that command instead.", + owner: "Workers: Authoring and Testing", + status: "stable", + }, + args: { + name: { describe: "Name of the worker", type: "string", requiresArg: true, - }) - .positional("deployment-id", { + }, + "deployment-id": { describe: "Deprecated. Deployment ID is now referred to as Version ID. Please use `wrangler versions view [version-id]` instead.", type: "string", requiresArg: true, - }); -} - -export async function versionsDeploymentsViewHandler( - args: VersionsDeploymentsViewArgs -) { - await printWranglerBanner(); - - if (args.deploymentId === undefined) { - throw new UserError( - "`wrangler deployments view` has been renamed `wrangler deployments status`. Please use that command instead." - ); - } else { - throw new UserError( - "`wrangler deployments view ` has been renamed `wrangler versions view [version-id]`. Please use that command instead." - ); - } -} + }, + }, + handler: async function versionsDeploymentsViewHandler(args) { + if (args.deploymentId === undefined) { + throw new UserError( + "`wrangler deployments view` has been renamed `wrangler deployments status`. Please use that command instead." + ); + } else { + throw new UserError( + "`wrangler deployments view ` has been renamed `wrangler versions view [version-id]`. Please use that command instead." + ); + } + }, +}); diff --git a/packages/wrangler/src/versions/index.ts b/packages/wrangler/src/versions/index.ts index fb3009d22e30..4eb9635dfa0a 100644 --- a/packages/wrangler/src/versions/index.ts +++ b/packages/wrangler/src/versions/index.ts @@ -1,356 +1,11 @@ -import assert from "node:assert"; -import path from "node:path"; -import { processAssetsArg, validateAssetsArgsAndConfig } from "../assets"; -import { findWranglerToml, 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 wrangler.toml 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 && findWranglerToml(path.dirname(args.script))); - const projectRoot = configPath && path.dirname(configPath); - 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 = processAssetsArg(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 wrangler.toml.\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(entry.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, - projectRoot, - - 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 { defineNamespace } from "../core"; + +defineNamespace({ + command: "wrangler versions", + metadata: { + description: + "🫧 List, view, upload and deploy Versions of your Worker to Cloudflare", + status: "open-beta", + owner: "Workers: Authoring and Testing", + }, +}); diff --git a/packages/wrangler/src/versions/list.ts b/packages/wrangler/src/versions/list.ts index 3a7926d8c5ce..90cc4a3f2568 100644 --- a/packages/wrangler/src/versions/list.ts +++ b/packages/wrangler/src/versions/list.ts @@ -1,95 +1,83 @@ -import path from "path"; import { logRaw } from "@cloudflare/cli"; -import { findWranglerToml, readConfig } from "../config"; +import { defineCommand } from "../core"; 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", { +defineCommand({ + command: "wrangler versions list", + 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, - }); -} - -export async function versionsListHandler(args: VersionsListArgs) { - if (!args.json) { - await printWranglerBanner(); - } - - const config = getConfig(args); - await metrics.sendMetricsEvent( - "list worker versions", - { json: args.json }, - { - sendMetrics: config.send_metrics, + }, + }, + handler: async function versionsListHandler(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( + "list worker versions", + { json: args.json }, + { + sendMetrics: config.send_metrics, + } ); - } - 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 accountId = await requireAuth(config); + const workerName = args.name ?? config.name; - if (args.json) { - logRaw(JSON.stringify(versions, 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 = ""`' + ); + } - 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, - }); + const versionCache: VersionCache = new Map(); + const versions = ( + await fetchDeployableVersions(accountId, workerName, versionCache) + ).sort((a, b) => + a.metadata.created_on.localeCompare(b.metadata.created_on) + ); - logRaw(formattedVersion); - logRaw(``); - } -} + if (args.json) { + logRaw(JSON.stringify(versions, null, 2)); + return; + } -export function getConfig( - args: Pick -) { - const configPath = - args.config || (args.name && findWranglerToml(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..39399c1b2c92 100644 --- a/packages/wrangler/src/versions/rollback/index.ts +++ b/packages/wrangler/src/versions/rollback/index.ts @@ -1,5 +1,6 @@ import * as cli from "@cloudflare/cli"; import { spinnerWhile } from "@cloudflare/cli/interactive"; +import { defineCommand, defineNamespace } from "../../core"; import { confirm, prompt } from "../../dialogs"; import { UserError } from "../../errors"; import { logger } from "../../logger"; @@ -7,147 +8,132 @@ import { APIError } from "../../parse"; import { requireAuth } from "../../user"; import { createDeployment, fetchLatestDeployments, fetchVersion } from "../api"; import { printLatestDeployment, printVersions } from "../deploy"; -import { getConfig } from "../list"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; import type { VersionId } from "../types"; export const CANNOT_ROLLBACK_WITH_MODIFIED_SECERT_CODE = 10220; -type VersionsRollbackArgs = StrictYargsOptionsToInterface< - typeof versionsRollbackOptions ->; - -export default function registerVersionsRollbackCommand( - yargs: CommonYargsArgv, - description = "🔙 Rollback to a Worker Version" -) { - return yargs.command( - "rollback [version-id]", - description, - versionsRollbackOptions, - versionsRollbackHandler - ); -} - -function versionsRollbackOptions(rollbackYargs: CommonYargsArgv) { - return rollbackYargs - .positional("version-id", { +defineCommand({ + command: "wrangler rollback", + metadata: { + description: "🔙 Rollback to a Worker Version", + owner: "Workers: Authoring and Testing", + status: "open-beta", + }, + args: { + "version-id": { describe: "The ID of the Worker Version to rollback to", type: "string", demandOption: false, - }) - .option("name", { + }, + name: { describe: "The name of your worker", type: "string", - }) - .option("message", { + }, + message: { alias: "m", describe: "The reason for this rollback", type: "string", default: undefined, - }) - .option("yes", { + }, + yes: { alias: "y", describe: "Automatically accept defaults to prompts", type: "boolean", default: false, - }); -} + }, + }, + positionalArgs: ["version-id"], + handler: async function versionsRollbackHandler(args, { config }) { + 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 = ""`' + ); + } -async function versionsRollbackHandler(args: VersionsRollbackArgs) { - const config = getConfig(args); - const accountId = await requireAuth(config); - const workerName = args.name ?? config.name; + await printLatestDeployment(accountId, workerName, new Map()); - 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 versionId = + args.versionId ?? + (await spinnerWhile({ + promise: fetchDefaultRollbackVersionId(accountId, workerName), + startMessage: "Finding latest stable Worker Version to rollback to", + endMessage: "", + })); - await printLatestDeployment(accountId, workerName, new Map()); + const message = await prompt( + "Please provide an optional message for this rollback (120 characters max)", + { + defaultValue: args.message ?? "Rollback", + } + ); - const versionId = - args.versionId ?? - (await spinnerWhile({ - promise: fetchDefaultRollbackVersionId(accountId, workerName), - startMessage: "Finding latest stable Worker Version to rollback to", - endMessage: "", - })); + const version = await fetchVersion(accountId, workerName, versionId); + cli.warn( + `You are about to rollback to Worker Version ${versionId}.\nThis will immediately replace the current deployment and become the active deployment across all your deployed triggers.\nHowever, your local development environment will not be affected by this rollback.\nRolling back to a previous deployment will not rollback any of the bound resources (Durable Object, D1, R2, KV, etc).`, + { multiline: true, shape: cli.shapes.leftT } + ); + const rollbackTraffic = new Map([[versionId, 100]]); + printVersions([version], rollbackTraffic); - const message = await prompt( - "Please provide an optional message for this rollback (120 characters max)", - { - defaultValue: args.message ?? "Rollback", + const confirmed = await confirm( + "Are you sure you want to deploy this Worker Version to 100% of traffic?", + { defaultValue: true } + ); + if (!confirmed) { + cli.cancel("Aborting rollback..."); + return; } - ); - const version = await fetchVersion(accountId, workerName, versionId); - cli.warn( - `You are about to rollback to Worker Version ${versionId}.\nThis will immediately replace the current deployment and become the active deployment across all your deployed triggers.\nHowever, your local development environment will not be affected by this rollback.\nRolling back to a previous deployment will not rollback any of the bound resources (Durable Object, D1, R2, KV, etc).`, - { multiline: true, shape: cli.shapes.leftT } - ); - const rollbackTraffic = new Map([[versionId, 100]]); - printVersions([version], rollbackTraffic); - - const confirmed = await confirm( - "Are you sure you want to deploy this Worker Version to 100% of traffic?", - { defaultValue: true } - ); - if (!confirmed) { - cli.cancel("Aborting rollback..."); - return; - } - - logger.log("Performing rollback..."); - try { - await createDeployment(accountId, workerName, rollbackTraffic, message); - } catch (e) { - if ( - e instanceof APIError && - e.code === CANNOT_ROLLBACK_WITH_MODIFIED_SECERT_CODE - ) { - // This is not great but is the best way I could think to handle for now - const errorMsg = e.notes[0].text.replace( - ` [code: ${CANNOT_ROLLBACK_WITH_MODIFIED_SECERT_CODE}]`, - "" - ); - const targetString = "The following secrets have changed:"; - const changedSecrets = errorMsg - .substring(errorMsg.indexOf(targetString) + targetString.length + 1) - .split(", "); - - const secretConfirmation = await confirm( - `The following secrets have changed since version ${versionId} was deployed. ` + - `Please confirm you wish to continue with the rollback\n` + - changedSecrets.map((secret) => ` * ${secret}`).join("\n") - ); - - if (secretConfirmation) { - await createDeployment( - accountId, - workerName, - rollbackTraffic, - message, - true + logger.log("Performing rollback..."); + try { + await createDeployment(accountId, workerName, rollbackTraffic, message); + } catch (e) { + if ( + e instanceof APIError && + e.code === CANNOT_ROLLBACK_WITH_MODIFIED_SECERT_CODE + ) { + // This is not great but is the best way I could think to handle for now + const errorMsg = e.notes[0].text.replace( + ` [code: ${CANNOT_ROLLBACK_WITH_MODIFIED_SECERT_CODE}]`, + "" ); + const targetString = "The following secrets have changed:"; + const changedSecrets = errorMsg + .substring(errorMsg.indexOf(targetString) + targetString.length + 1) + .split(", "); + + const secretConfirmation = await confirm( + `The following secrets have changed since version ${versionId} was deployed. ` + + `Please confirm you wish to continue with the rollback\n` + + changedSecrets.map((secret) => ` * ${secret}`).join("\n") + ); + + if (secretConfirmation) { + await createDeployment( + accountId, + workerName, + rollbackTraffic, + message, + true + ); + } else { + cli.cancel("Aborting rollback..."); + } } else { - cli.cancel("Aborting rollback..."); + throw e; } - } else { - throw e; } - } - cli.success( - `Worker Version ${versionId} has been deployed to 100% of traffic.` - ); + cli.success( + `Worker Version ${versionId} has been deployed to 100% of traffic.` + ); - logger.log("\nCurrent Version ID: " + versionId); -} + logger.log("\nCurrent Version ID: " + versionId); + }, +}); async function fetchDefaultRollbackVersionId( accountId: string, diff --git a/packages/wrangler/src/versions/secrets/bulk.ts b/packages/wrangler/src/versions/secrets/bulk.ts index 2a2407e9706f..6d955c0836f0 100644 --- a/packages/wrangler/src/versions/secrets/bulk.ts +++ b/packages/wrangler/src/versions/secrets/bulk.ts @@ -1,7 +1,7 @@ import path from "node:path"; import readline from "node:readline"; import { fetchResult } from "../../cfetch"; -import { readConfig } from "../../config"; +import { defineCommand } from "../../core"; import { UserError } from "../../errors"; import { getLegacyScriptName } from "../../index"; import { logger } from "../../logger"; @@ -10,120 +10,118 @@ 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", { +defineCommand({ + command: "wrangler versions secret bulk", + metadata: { + description: "Create or update a secret variable for a Worker", + owner: "Workers: Authoring and Testing", + status: "open-beta", + }, + 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 wrangler.toml, 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 wrangler.toml, 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 281a15490a02..185ad8ca98ff 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 { readConfig } from "../../config"; +import { defineCommand } from "../../core"; 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", { +defineCommand({ + command: "wrangler versions secrets delete", + metadata: { + description: "Delete a secret variable from a Worker", + owner: "Workers: Authoring and Testing", + status: "open-beta", + }, + 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 wrangler.toml, 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 wrangler.toml, 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..726596054eed 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 { defineNamespace } from "../../core"; 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,16 @@ 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 - ); -} +defineNamespace({ + command: "wrangler versions secret", + metadata: { + description: "Generate a secret that can be referenced in a Worker", + status: "open-beta", + 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 e002123a6f2f..afbc2012fd35 100644 --- a/packages/wrangler/src/versions/secrets/list.ts +++ b/packages/wrangler/src/versions/secrets/list.ts @@ -1,94 +1,99 @@ import { fetchResult } from "../../cfetch"; import { readConfig } from "../../config"; +import { defineCommand } from "../../core"; 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", { +defineCommand({ + command: "wrangler versions secret list", + metadata: { + description: "List the secrets currently deployed", + owner: "Workers: Authoring and Testing", + status: "open-beta", + }, + 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 wrangler.toml, 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 wrangler.toml, 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 ff5c03779ab8..5848f09bfb74 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 { readConfig } from "../../config"; +import { defineCommand } from "../../core"; 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", { +defineCommand({ + command: "wrangler versions secret put", + metadata: { + description: "Create or update a secret variable for a Worker", + owner: "Workers: Authoring and Testing", + status: "open-beta", + }, + 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 wrangler.toml, 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 wrangler.toml, 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 3811303591da..ad96342ebe6f 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -1,9 +1,15 @@ +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 { + processAssetsArg, + syncAssets, + validateAssetsArgsAndConfig, +} from "../assets"; import { fetchResult } from "../cfetch"; -import { printBindings } from "../config"; +import { findWranglerToml, printBindings } from "../config"; +import { defineCommand } from "../core"; import { bundleWorker } from "../deployment-bundle/bundle"; import { printBundleSize, @@ -11,6 +17,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, @@ -25,9 +32,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"; @@ -36,6 +47,9 @@ import { getSourceMappedString, maybeRetrieveFileSourceMap, } from "../sourcemap"; +import { printWranglerBanner } from "../update-check"; +import { requireAuth } from "../user"; +import { collectKeyValues } from "../utils/collectKeyValues"; import { retryOnError } from "../utils/retry"; import type { AssetsOptions } from "../assets"; import type { Config } from "../config"; @@ -70,7 +84,6 @@ type Props = { noBundle: boolean | undefined; keepVars: boolean | undefined; projectRoot: string | undefined; - tag: string | undefined; message: string | undefined; }; @@ -112,7 +125,285 @@ function errIsStartupErr(err: unknown): err is ParseError & { code: 10021 } { return false; } -export default async function versionsUpload(props: Props): Promise<{ +defineCommand({ + command: "wrangler versions upload", + 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, + }, + }, + handler: async function versionsUploadHandler(args, { config }) { + await printWranglerBanner(); + + const configPath = + args.config || + (args.script && findWranglerToml(path.dirname(args.script))); + const projectRoot = configPath && path.dirname(configPath); + + 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 = processAssetsArg(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 wrangler.toml.\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(entry.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, + projectRoot, + + 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 versionsUpload(props: Props): Promise<{ versionId: string | null; workerTag: string | null; versionPreviewUrl?: string | undefined; @@ -639,3 +930,11 @@ async function noBundleWorker( bundleType: getBundleType(entry.format), }; } + +async function standardPricingWarning(config: Config) { + if (config.usage_model !== undefined) { + logger.warn( + "The `usage_model` defined in wrangler.toml is deprecated and no longer used. Visit our developer docs for details: https://developers.cloudflare.com/workers/wrangler/configuration/#usage-model" + ); + } +} diff --git a/packages/wrangler/src/versions/utils/get-config.ts b/packages/wrangler/src/versions/utils/get-config.ts new file mode 100644 index 000000000000..745fb2611159 --- /dev/null +++ b/packages/wrangler/src/versions/utils/get-config.ts @@ -0,0 +1,14 @@ +import path from "path"; +import { findWranglerToml, readConfig } from "../../config"; + +export function getConfig< + T extends { + name?: string; + config?: string; + experimentalJsonConfig?: boolean; + }, +>(args: Pick) { + const configPath = + args.config || (args.name && findWranglerToml(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..ddc1194bb553 100644 --- a/packages/wrangler/src/versions/view.ts +++ b/packages/wrangler/src/versions/view.ts @@ -1,141 +1,143 @@ import { logRaw } from "@cloudflare/cli"; +import { defineCommand } from "../core"; 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", { +defineCommand({ + command: "wrangler versions view", + metadata: { + description: "View the details of a specific Worker version", + 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;