From f0be6eaaad33ff97030584ad86dc80a443e51342 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:53:09 +0100 Subject: [PATCH 01/16] start from workflows branch, squashed --- fixtures/workflow/CHANGELOG.md | 7 + fixtures/workflow/package.json | 15 + fixtures/workflow/src/index.ts | 50 ++++ fixtures/workflow/tsconfig.json | 13 + fixtures/workflow/wrangler.toml | 10 + packages/wrangler/package.json | 2 + .../__tests__/navigator-user-agent.test.ts | 2 + .../src/__tests__/type-generation.test.ts | 1 + .../pages/create-worker-bundle-contents.ts | 1 + .../api/startDevWorker/BundlerController.ts | 2 + .../wrangler/src/api/startDevWorker/types.ts | 2 + .../wrangler/src/api/startDevWorker/utils.ts | 7 + packages/wrangler/src/config/config.ts | 1 + packages/wrangler/src/config/environment.ts | 22 ++ packages/wrangler/src/config/index.ts | 18 ++ packages/wrangler/src/config/validation.ts | 67 +++++ packages/wrangler/src/deploy/deploy.ts | 2 + .../wrangler/src/deployment-bundle/bundle.ts | 19 +- .../create-worker-upload-form.ts | 17 ++ .../wrangler/src/deployment-bundle/worker.ts | 8 + packages/wrangler/src/dev.tsx | 4 +- packages/wrangler/src/dev/dev.tsx | 1 + packages/wrangler/src/dev/start-server.ts | 9 +- packages/wrangler/src/dev/use-esbuild.ts | 7 + packages/wrangler/src/index.ts | 6 + packages/wrangler/src/init.ts | 13 + .../src/pages/functions/buildPlugin.ts | 1 + .../src/pages/functions/buildWorker.ts | 2 + packages/wrangler/src/secret/index.ts | 1 + packages/wrangler/src/triggers/deploy.ts | 17 ++ packages/wrangler/src/versions/upload.ts | 2 + .../wrangler/src/workflows/commands/delete.ts | 33 +++ .../src/workflows/commands/describe.ts | 62 ++++ .../workflows/commands/instances/describe.ts | 280 ++++++++++++++++++ .../src/workflows/commands/instances/index.ts | 29 ++ .../src/workflows/commands/instances/list.ts | 95 ++++++ .../workflows/commands/instances/terminate.ts | 66 +++++ .../wrangler/src/workflows/commands/list.ts | 66 +++++ .../src/workflows/commands/trigger.ts | 57 ++++ packages/wrangler/src/workflows/types.ts | 106 +++++++ packages/wrangler/src/workflows/utils.ts | 70 +++++ packages/wrangler/src/workflows/workflows.ts | 54 ++++ pnpm-lock.yaml | 39 ++- 43 files changed, 1279 insertions(+), 7 deletions(-) create mode 100644 fixtures/workflow/CHANGELOG.md create mode 100644 fixtures/workflow/package.json create mode 100644 fixtures/workflow/src/index.ts create mode 100644 fixtures/workflow/tsconfig.json create mode 100644 fixtures/workflow/wrangler.toml create mode 100644 packages/wrangler/src/workflows/commands/delete.ts create mode 100644 packages/wrangler/src/workflows/commands/describe.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/describe.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/index.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/list.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/terminate.ts create mode 100644 packages/wrangler/src/workflows/commands/list.ts create mode 100644 packages/wrangler/src/workflows/commands/trigger.ts create mode 100644 packages/wrangler/src/workflows/types.ts create mode 100644 packages/wrangler/src/workflows/utils.ts create mode 100644 packages/wrangler/src/workflows/workflows.ts diff --git a/fixtures/workflow/CHANGELOG.md b/fixtures/workflow/CHANGELOG.md new file mode 100644 index 000000000000..912ae7b44279 --- /dev/null +++ b/fixtures/workflow/CHANGELOG.md @@ -0,0 +1,7 @@ +# worker-ts + +## 0.0.1 + +### Patch Changes + +- [#3588](https://github.com/cloudflare/workers-sdk/pull/3588) [`64631d8b`](https://github.com/cloudflare/workers-sdk/commit/64631d8b59572f49d65325d8f6fec098c5e912b9) Thanks [@penalosa](https://github.com/penalosa)! - fix: Preserve email handlers when applying middleware to user workers. diff --git a/fixtures/workflow/package.json b/fixtures/workflow/package.json new file mode 100644 index 000000000000..9fc4ce659562 --- /dev/null +++ b/fixtures/workflow/package.json @@ -0,0 +1,15 @@ +{ + "name": "my-workflow", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "start": "wrangler dev --x-dev-env" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240909.0", + "wrangler": "workspace:*" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/fixtures/workflow/src/index.ts b/fixtures/workflow/src/index.ts new file mode 100644 index 000000000000..2326cf0b78d1 --- /dev/null +++ b/fixtures/workflow/src/index.ts @@ -0,0 +1,50 @@ +import { + WorkerEntrypoint, + Workflow, + WorkflowEvent, + WorkflowStep, +} from "cloudflare:workers"; + +type Params = { + name: string; +}; +export class Demo extends Workflow<{}, Params> { + async run(events: Array>, step: WorkflowStep) { + const { timestamp, payload } = events[0]; + const result = await step.do("First step", async function () { + return { + output: "First step result", + }; + }); + + await step.sleep("Wait", "1 minute"); + + const result2 = await step.do("Second step", async function () { + return { + output: "Second step result", + }; + }); + + return { + result, + result2, + timestamp, + payload, + }; + } +} + +type Env = { + WORKFLOW: { + create: (id: string) => { + pause: () => {}; + }; + }; +}; +export default class extends WorkerEntrypoint { + async fetch() { + const handle = await this.env.WORKFLOW.create(crypto.randomUUID()); + await handle.pause(); + return new Response(); + } +} diff --git a/fixtures/workflow/tsconfig.json b/fixtures/workflow/tsconfig.json new file mode 100644 index 000000000000..2431bac6e945 --- /dev/null +++ b/fixtures/workflow/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "es2022", + "types": ["@cloudflare/workers-types/experimental"], + "noEmit": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/fixtures/workflow/wrangler.toml b/fixtures/workflow/wrangler.toml new file mode 100644 index 000000000000..ee5fe0862416 --- /dev/null +++ b/fixtures/workflow/wrangler.toml @@ -0,0 +1,10 @@ +#:schema node_modules/wrangler/config-schema.json +name = "my-workflow-demo" +main = "src/index.ts" +compatibility_date = "2024-09-02" +compatibility_flags = ["experimental"] + +[[workflows]] +binding = "WORKFLOW" +name = "my-workflow" +class_name = "Demo" \ No newline at end of file diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 27151af3d295..2ffedcf65f49 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -75,7 +75,9 @@ "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", "chokidar": "^3.5.3", + "date-fns": "^4.1.0", "esbuild": "0.17.19", + "itty-time": "^1.0.6", "miniflare": "workspace:*", "nanoid": "^3.3.3", "path-to-regexp": "^6.3.0", diff --git a/packages/wrangler/src/__tests__/navigator-user-agent.test.ts b/packages/wrangler/src/__tests__/navigator-user-agent.test.ts index cda656d441c1..34c1e3ca0759 100644 --- a/packages/wrangler/src/__tests__/navigator-user-agent.test.ts +++ b/packages/wrangler/src/__tests__/navigator-user-agent.test.ts @@ -116,6 +116,7 @@ describe("defineNavigatorUserAgent is respected", () => { serveLegacyAssetsFromWorker: false, mockAnalyticsEngineDatasets: [], doBindings: [], + workflowBindings: [], define: {}, alias: {}, checkFetch: false, @@ -175,6 +176,7 @@ describe("defineNavigatorUserAgent is respected", () => { moduleCollector: noopModuleCollector, serveLegacyAssetsFromWorker: false, doBindings: [], + workflowBindings: [], define: {}, alias: {}, mockAnalyticsEngineDatasets: [], diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index efceb1778477..94665fa07c26 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -164,6 +164,7 @@ const bindingsConfigMock: Omit< }, ], }, + workflows: [], r2_buckets: [ { binding: "R2_BUCKET_BINDING", diff --git a/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts b/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts index be4af53339ba..fec92c43005b 100644 --- a/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts +++ b/packages/wrangler/src/api/pages/create-worker-bundle-contents.ts @@ -51,6 +51,7 @@ function createWorkerBundleFormData( ai: config?.ai, version_metadata: config?.version_metadata, durable_objects: config?.durable_objects, + workflows: config?.workflows, queues: config?.queues.producers?.map((producer) => { return { binding: producer.binding, queue_name: producer.queue }; }), diff --git a/packages/wrangler/src/api/startDevWorker/BundlerController.ts b/packages/wrangler/src/api/startDevWorker/BundlerController.ts index ed663ec8443e..4e7f8ee5524e 100644 --- a/packages/wrangler/src/api/startDevWorker/BundlerController.ts +++ b/packages/wrangler/src/api/startDevWorker/BundlerController.ts @@ -108,6 +108,7 @@ export class BundlerController extends Controller { serveLegacyAssetsFromWorker: Boolean( config.legacy?.legacyAssets && !config.dev?.remote ), + workflowBindings: bindings?.workflows ?? [], doBindings: bindings?.durable_objects?.bindings ?? [], jsxFactory: config.build.jsxFactory, jsxFragment: config.build.jsxFactory, @@ -238,6 +239,7 @@ export class BundlerController extends Controller { noBundle: !config.build?.bundle, findAdditionalModules: config.build?.findAdditionalModules, durableObjects: bindings?.durable_objects ?? { bindings: [] }, + workflows: bindings?.workflows ?? [], mockAnalyticsEngineDatasets: bindings.analytics_engine_datasets ?? [], local: !config.dev?.remote, // startDevWorker only applies to "dev" diff --git a/packages/wrangler/src/api/startDevWorker/types.ts b/packages/wrangler/src/api/startDevWorker/types.ts index 868b6e44d24a..99590f178b59 100644 --- a/packages/wrangler/src/api/startDevWorker/types.ts +++ b/packages/wrangler/src/api/startDevWorker/types.ts @@ -25,6 +25,7 @@ import type { CfService, CfUnsafe, CfVectorize, + CfWorkflow, } from "../../deployment-bundle/worker"; import type { WorkerRegistry } from "../../dev-registry"; import type { CfAccount } from "../../dev/create-worker-preview"; @@ -254,6 +255,7 @@ export type Binding = | { type: "version_metadata" } | { type: "data_blob"; source: BinaryFile } | ({ type: "durable_object_namespace" } & BindingOmit) + | ({ type: "workflow" } & BindingOmit) | ({ type: "queue" } & BindingOmit) | ({ type: "r2_bucket" } & BindingOmit) | ({ type: "d1" } & Omit) diff --git a/packages/wrangler/src/api/startDevWorker/utils.ts b/packages/wrangler/src/api/startDevWorker/utils.ts index e3d3f019b59b..a6c2c16ed1d3 100644 --- a/packages/wrangler/src/api/startDevWorker/utils.ts +++ b/packages/wrangler/src/api/startDevWorker/utils.ts @@ -164,6 +164,12 @@ export function convertCfWorkerInitBindingstoBindings( } break; } + case "workflows": { + for (const { binding, ...x } of info) { + output[binding] = { type: "workflow", ...x }; + } + break; + } case "queues": { for (const { binding, ...x } of info) { output[binding] = { type: "queue", ...x }; @@ -278,6 +284,7 @@ export async function convertBindingsToCfWorkerInitBindings( durable_objects: undefined, queues: undefined, r2_buckets: undefined, + workflows: undefined, d1_databases: undefined, vectorize: undefined, hyperdrive: undefined, diff --git a/packages/wrangler/src/config/config.ts b/packages/wrangler/src/config/config.ts index e535efb98752..fe991c1c49d2 100644 --- a/packages/wrangler/src/config/config.ts +++ b/packages/wrangler/src/config/config.ts @@ -328,6 +328,7 @@ export const defaultWranglerConfig: Config = { d1_databases: [], vectorize: [], hyperdrive: [], + workflows: [], services: [], analytics_engine_datasets: [], ai: undefined, diff --git a/packages/wrangler/src/config/environment.ts b/packages/wrangler/src/config/environment.ts index 1066efae3106..d5663db5ad22 100644 --- a/packages/wrangler/src/config/environment.ts +++ b/packages/wrangler/src/config/environment.ts @@ -372,6 +372,17 @@ export type DurableObjectBindings = { environment?: string; }[]; +export type WorkflowBinding = { + /** The name of the binding used to refer to the Workflow */ + binding: string; + /** The name of the Workflow */ + name: string; + /** The exported class name of the Workflow */ + class_name: string; + /** The script where the Workflow is defined (if it's external to this Worker) */ + script_name?: string; +}; + /** * The `EnvironmentNonInheritable` interface declares all the configuration fields for an environment * that cannot be inherited from the top-level environment, and must be defined specifically. @@ -417,6 +428,17 @@ export interface EnvironmentNonInheritable { bindings: DurableObjectBindings; }; + /** + * A list of workflows that your Worker should be bound to. + * + * NOTE: This field is not automatically inherited from the top level environment, + * and so must be specified in every named environment. + * + * @default `[]` + * @nonInheritable + */ + workflows: WorkflowBinding[]; + /** * Cloudchamber configuration * diff --git a/packages/wrangler/src/config/index.ts b/packages/wrangler/src/config/index.ts index e403044679d7..1f1ad2bf9c5c 100644 --- a/packages/wrangler/src/config/index.ts +++ b/packages/wrangler/src/config/index.ts @@ -217,6 +217,7 @@ export function printBindings( const { data_blobs, durable_objects, + workflows, kv_namespaces, send_email, queues, @@ -278,6 +279,23 @@ export function printBindings( }); } + if (workflows !== undefined && workflows.length > 0) { + output.push({ + type: "Workflows", + entries: workflows.map(({ class_name, script_name, binding }) => { + let value = class_name; + if (script_name) { + value += ` (defined in ${script_name})`; + } + + return { + key: binding, + value, + }; + }), + }); + } + if (kv_namespaces !== undefined && kv_namespaces.length > 0) { output.push({ type: "KV Namespaces", diff --git a/packages/wrangler/src/config/validation.ts b/packages/wrangler/src/config/validation.ts index 80b2ac2774f9..08075d09b94d 100644 --- a/packages/wrangler/src/config/validation.ts +++ b/packages/wrangler/src/config/validation.ts @@ -1286,6 +1286,16 @@ function normalizeAndValidateEnvironment( bindings: [], } ), + workflows: notInheritable( + diagnostics, + topLevelEnv, + rawConfig, + rawEnv, + envName, + "workflows", + validateBindingArray(envName, validateWorkflowBinding), + [] + ), migrations: inheritable( diagnostics, topLevelEnv, @@ -2008,6 +2018,63 @@ const validateDurableObjectBinding: ValidatorFn = ( return isValid; }; +/** + * Check that the given field is a valid "workflow" binding object. + */ +const validateWorkflowBinding: ValidatorFn = (_diagnostics, _field, _value) => { + // if (typeof value !== "object" || value === null) { + // diagnostics.errors.push( + // `Expected "${field}" to be an object but got ${JSON.stringify(value)}` + // ); + // return false; + // } + + // Workflow bindings must have a name, class_name and binding, and optionally a script_name and an environment. + const isValid = true; + // if (!isRequiredProperty(value, "name", "string")) { + // diagnostics.errors.push(`binding should have a string "name" field.`); + // isValid = false; + // } + // if (!isRequiredProperty(value, "binding", "string")) { + // diagnostics.errors.push(`binding should have a string "binding" field.`); + // isValid = false; + // } + // if (!isRequiredProperty(value, "class_name", "string")) { + // diagnostics.errors.push(`binding should have a string "class_name" field.`); + // isValid = false; + // } + // if (!isOptionalProperty(value, "script_name", "string")) { + // diagnostics.errors.push( + // `the field "script_name", when present, should be a string.` + // ); + // isValid = false; + // } + // // environment requires a script_name + // if (!isOptionalProperty(value, "environment", "string")) { + // diagnostics.errors.push( + // `the field "environment", when present, should be a string.` + // ); + // isValid = false; + // } + + // if ("environment" in value && !("script_name" in value)) { + // diagnostics.errors.push( + // `binding should have a "script_name" field if "environment" is present.` + // ); + // isValid = false; + // } + + // validateAdditionalProperties(diagnostics, field, Object.keys(value), [ + // "class_name", + // "environment", + // "name", + // "script_name", + // "binding", + // ]); + + return isValid; +}; + const validateCflogfwdrObject: (env: string) => ValidatorFn = (envName) => (diagnostics, field, value, topLevelEnv) => { //validate the bindings property first, as this also validates that it's an object, etc. diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index 1f2addecf685..f8f708a9cdd2 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -580,6 +580,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m serveLegacyAssetsFromWorker: !props.isWorkersSite && Boolean(props.legacyAssetPaths), doBindings: config.durable_objects.bindings, + workflowBindings: config.workflows ?? [], jsxFactory, jsxFragment, tsconfig: props.tsconfig ?? config.tsconfig, @@ -674,6 +675,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m }, data_blobs: config.data_blobs, durable_objects: config.durable_objects, + workflows: config.workflows, queues: config.queues.producers?.map((producer) => { return { binding: producer.binding, queue_name: producer.queue }; }), diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts index dc6c462e219d..3727715c8f5f 100644 --- a/packages/wrangler/src/deployment-bundle/bundle.ts +++ b/packages/wrangler/src/deployment-bundle/bundle.ts @@ -25,7 +25,10 @@ import { standardURLPlugin } from "./esbuild-plugins/standard-url"; import { writeAdditionalModules } from "./find-additional-modules"; import { noopModuleCollector } from "./module-collection"; import type { Config } from "../config"; -import type { DurableObjectBindings } from "../config/environment"; +import type { + DurableObjectBindings, + WorkflowBinding, +} from "../config/environment"; import type { MiddlewareLoader } from "./apply-middleware"; import type { Entry } from "./entry"; import type { ModuleCollector } from "./module-collection"; @@ -115,6 +118,7 @@ export type BundleOptions = { legacyAssets?: Config["legacy_assets"]; bypassAssetCache?: boolean; doBindings: DurableObjectBindings; + workflowBindings: WorkflowBinding[]; jsxFactory?: string; jsxFragment?: string; entryName?: string; @@ -151,6 +155,7 @@ export async function bundleWorker( additionalModules = [], serveLegacyAssetsFromWorker, doBindings, + workflowBindings, jsxFactory, jsxFragment, entryName, @@ -507,6 +512,18 @@ export async function bundleWorker( ); } + const notExportedWorkflows = workflowBindings + .filter((x) => !x.script_name && !entryPoint.exports.includes(x.class_name)) + .map((x) => x.class_name); + if (notExportedWorkflows.length) { + const relativePath = path.relative(process.cwd(), entryFile); + throw new UserError( + `Your Worker depends on the following Workflows, which are not exported in your entrypoint file: ${notExportedWorkflows.join( + ", " + )}.\nYou should export these objects from your entrypoint, ${relativePath}.` + ); + } + const bundleType = entryPoint.exports.length > 0 ? "esm" : "commonjs"; const sourceMapPath = Object.keys(result.metafile.outputs).filter((_path) => diff --git a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts index 111936e25b71..25b3b47ce40b 100644 --- a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts +++ b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts @@ -76,6 +76,13 @@ export type WorkerMetadataBinding = script_name?: string; environment?: string; } + | { + type: "workflow"; + name: string; + workflow_name: string; + class_name: string; + script_name?: string; + } | { type: "queue"; name: string; queue_name: string; delivery_delay?: number } | { type: "r2_bucket"; @@ -247,6 +254,16 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData { } ); + bindings.workflows?.forEach(({ binding, name, class_name, script_name }) => { + metadataBindings.push({ + type: "workflow", + name: binding, + workflow_name: name, + class_name, + ...(script_name && { script_name }), + }); + }); + bindings.queues?.forEach(({ binding, queue_name, delivery_delay }) => { metadataBindings.push({ type: "queue", diff --git a/packages/wrangler/src/deployment-bundle/worker.ts b/packages/wrangler/src/deployment-bundle/worker.ts index e1d772407bb0..e78077c3481c 100644 --- a/packages/wrangler/src/deployment-bundle/worker.ts +++ b/packages/wrangler/src/deployment-bundle/worker.ts @@ -152,6 +152,13 @@ export interface CfDurableObject { environment?: string; } +export interface CfWorkflow { + name: string; + class_name: string; + binding: string; + script_name?: string; +} + export interface CfQueue { binding: string; queue_name: string; @@ -323,6 +330,7 @@ export interface CfWorkerInit { version_metadata: CfVersionMetadataBinding | undefined; data_blobs: CfDataBlobBindings | undefined; durable_objects: { bindings: CfDurableObject[] } | undefined; + workflows: CfWorkflow[] | undefined; queues: CfQueue[] | undefined; r2_buckets: CfR2Bucket[] | undefined; d1_databases: CfD1Database[] | undefined; diff --git a/packages/wrangler/src/dev.tsx b/packages/wrangler/src/dev.tsx index 4f9b040fe88d..109a91269d02 100644 --- a/packages/wrangler/src/dev.tsx +++ b/packages/wrangler/src/dev.tsx @@ -693,6 +693,7 @@ export async function startDev(args: StartDevOptions) { version_metadata: args.version_metadata, data_blobs: undefined, durable_objects: { bindings: args.durableObjects ?? [] }, + workflows: undefined, queues: undefined, r2_buckets: args.r2, d1_databases: args.d1Databases, @@ -1574,7 +1575,7 @@ export function getBindings( }), ]; - const bindings = { + const bindings: CfWorkerInit["bindings"] = { // top-level fields wasm_modules: configParam.wasm_modules, text_blobs: configParam.text_blobs, @@ -1593,6 +1594,7 @@ export function getBindings( durable_objects: { bindings: mergedDOBindings, }, + workflows: configParam.workflows, kv_namespaces: mergedKVBindings, queues: queuesBindings, r2_buckets: mergedR2Bindings, diff --git a/packages/wrangler/src/dev/dev.tsx b/packages/wrangler/src/dev/dev.tsx index 251ead69beed..e8210b851eba 100644 --- a/packages/wrangler/src/dev/dev.tsx +++ b/packages/wrangler/src/dev/dev.tsx @@ -543,6 +543,7 @@ function DevSession(props: DevSessionProps) { mockAnalyticsEngineDatasets: props.bindings.analytics_engine_datasets ?? [], legacyAssets: props.legacyAssetsConfig, durableObjects: props.bindings.durable_objects || { bindings: [] }, + workflows: props.bindings.workflows ?? [], local: props.local, // Enable the bundling to know whether we are using dev or deploy targetConsumer: "dev", diff --git a/packages/wrangler/src/dev/start-server.ts b/packages/wrangler/src/dev/start-server.ts index 4ba59da72251..913800d57717 100644 --- a/packages/wrangler/src/dev/start-server.ts +++ b/packages/wrangler/src/dev/start-server.ts @@ -37,7 +37,10 @@ import { startRemoteServer } from "./remote"; import { validateDevProps } from "./validate-dev-props"; import type { ProxyData, StartDevWorkerInput, Trigger } from "../api"; import type { Config } from "../config"; -import type { DurableObjectBindings } from "../config/environment"; +import type { + DurableObjectBindings, + WorkflowBinding, +} from "../config/environment"; import type { Entry } from "../deployment-bundle/entry"; import type { CfModule } from "../deployment-bundle/worker"; import type { WorkerRegistry } from "../dev-registry"; @@ -244,6 +247,7 @@ export async function startDevServer( testScheduled: props.testScheduled, local: props.local, doBindings: props.bindings.durable_objects?.bindings ?? [], + workflowBindings: props.bindings.workflows ?? [], mockAnalyticsEngineDatasets: props.bindings.analytics_engine_datasets ?? [], projectRoot: props.projectRoot, defineNavigatorUserAgent: isNavigatorDefined( @@ -418,6 +422,7 @@ async function runEsbuild({ testScheduled, local, doBindings, + workflowBindings, mockAnalyticsEngineDatasets, projectRoot, defineNavigatorUserAgent, @@ -441,6 +446,7 @@ async function runEsbuild({ testScheduled?: boolean; local: boolean; doBindings: DurableObjectBindings; + workflowBindings: WorkflowBinding[]; mockAnalyticsEngineDatasets: Config["analytics_engine_datasets"]; projectRoot: string | undefined; defineNavigatorUserAgent: boolean; @@ -488,6 +494,7 @@ async function runEsbuild({ local, testScheduled, doBindings, + workflowBindings, projectRoot, defineNavigatorUserAgent, }) diff --git a/packages/wrangler/src/dev/use-esbuild.ts b/packages/wrangler/src/dev/use-esbuild.ts index 7a073e27997b..6aa2545f3437 100644 --- a/packages/wrangler/src/dev/use-esbuild.ts +++ b/packages/wrangler/src/dev/use-esbuild.ts @@ -51,6 +51,7 @@ export type EsbuildBundleProps = { noBundle: boolean; findAdditionalModules: boolean | undefined; durableObjects: Config["durable_objects"]; + workflows: Config["workflows"]; mockAnalyticsEngineDatasets: Config["analytics_engine_datasets"]; local: boolean; targetConsumer: "dev" | "deploy"; @@ -81,6 +82,7 @@ export function runBuild( findAdditionalModules, mockAnalyticsEngineDatasets, durableObjects, + workflows, local, targetConsumer, testScheduled, @@ -105,6 +107,7 @@ export function runBuild( noBundle: boolean; findAdditionalModules: boolean | undefined; durableObjects: Config["durable_objects"]; + workflows: Config["workflows"]; mockAnalyticsEngineDatasets: Config["analytics_engine_datasets"]; local: boolean; targetConsumer: "dev" | "deploy"; @@ -183,6 +186,7 @@ export function runBuild( minify, nodejsCompatMode, doBindings: durableObjects.bindings, + workflowBindings: workflows, alias, define, checkFetch: true, @@ -270,6 +274,7 @@ export function useEsbuild({ findAdditionalModules, mockAnalyticsEngineDatasets, durableObjects, + workflows, local, targetConsumer, testScheduled, @@ -300,6 +305,7 @@ export function useEsbuild({ noBundle, findAdditionalModules, durableObjects, + workflows, mockAnalyticsEngineDatasets, local, targetConsumer, @@ -335,6 +341,7 @@ export function useEsbuild({ legacyAssets, mockAnalyticsEngineDatasets, durableObjects, + workflows, local, targetConsumer, testScheduled, diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index b6b1422beed3..0df0acca9384 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -77,6 +77,7 @@ import registerVersionsSubcommands from "./versions"; import registerVersionsDeploymentsSubcommands from "./versions/deployments"; import registerVersionsRollbackCommand from "./versions/rollback"; import { whoami } from "./whoami"; +import { workflows } from "./workflows/workflows"; import { asJson } from "./yargs-types"; import type { Config } from "./config"; import type { LoggerLevel } from "./logger"; @@ -608,6 +609,11 @@ export function createCLIParser(argv: string[]) { return ai(aiYargs.command(subHelp)); }); + // workflows + wrangler.command("workflows", false, (workflowArgs) => { + return workflows(workflowArgs.command(subHelp), subHelp); + }); + // pipelines wrangler.command("pipelines", false, (pipelinesYargs) => { return pipelines(pipelinesYargs.command(subHelp)); diff --git a/packages/wrangler/src/init.ts b/packages/wrangler/src/init.ts index 4d3728cd8cd8..bd56661fde0d 100644 --- a/packages/wrangler/src/init.ts +++ b/packages/wrangler/src/init.ts @@ -1248,6 +1248,19 @@ export async function mapBindings( metadata: configObj.unsafe?.metadata ?? undefined, }; break; + case "workflow": + { + configObj.workflows = [ + ...(configObj.workflows ?? []), + { + binding: binding.name, + name: binding.workflow_name, + class_name: binding.class_name, + script_name: binding.script_name, + }, + ]; + } + break; default: { configObj.unsafe = { bindings: [...(configObj.unsafe?.bindings ?? []), binding], diff --git a/packages/wrangler/src/pages/functions/buildPlugin.ts b/packages/wrangler/src/pages/functions/buildPlugin.ts index 867a8dfa3fa9..7f6c1b022b2e 100644 --- a/packages/wrangler/src/pages/functions/buildPlugin.ts +++ b/packages/wrangler/src/pages/functions/buildPlugin.ts @@ -52,6 +52,7 @@ export function buildPluginFromFunctions({ define: {}, alias: {}, doBindings: [], // Pages functions don't support internal Durable Objects + workflowBindings: [], // Pages functions don't support internal Workflows external, plugins: [ buildNotifierPlugin(onEnd), diff --git a/packages/wrangler/src/pages/functions/buildWorker.ts b/packages/wrangler/src/pages/functions/buildWorker.ts index 77a2ab984611..d6f670f60f25 100644 --- a/packages/wrangler/src/pages/functions/buildWorker.ts +++ b/packages/wrangler/src/pages/functions/buildWorker.ts @@ -79,6 +79,7 @@ export function buildWorkerFromFunctions({ }, alias: {}, doBindings: [], // Pages functions don't support internal Durable Objects + workflowBindings: [], // Pages functions don't support internal Workflows external, plugins: [buildNotifierPlugin(onEnd), assetsPlugin(buildOutputDirectory)], isOutfile: !outdir, @@ -159,6 +160,7 @@ export function buildRawWorker({ define: {}, alias: {}, doBindings: [], // Pages functions don't support internal Durable Objects + workflowBindings: [], // Pages functions don't support internal Workflows external, plugins: [ ...plugins, diff --git a/packages/wrangler/src/secret/index.ts b/packages/wrangler/src/secret/index.ts index 3dd1c202b6a3..865ed9e524aa 100644 --- a/packages/wrangler/src/secret/index.ts +++ b/packages/wrangler/src/secret/index.ts @@ -77,6 +77,7 @@ async function createDraftWorker({ send_email: [], vars: {}, durable_objects: { bindings: [] }, + workflows: [], queues: [], r2_buckets: [], d1_databases: [], diff --git a/packages/wrangler/src/triggers/deploy.ts b/packages/wrangler/src/triggers/deploy.ts index 50f1016056be..cf5fe499f36b 100644 --- a/packages/wrangler/src/triggers/deploy.ts +++ b/packages/wrangler/src/triggers/deploy.ts @@ -240,6 +240,23 @@ export default async function triggersDeploy( deployments.push(...updateConsumers); } + if (config.workflows?.length) { + for (const workflow of config.workflows) { + deployments.push( + fetchResult(`/accounts/${accountId}/workflows/${workflow.name}`, { + method: "PUT", + body: JSON.stringify({ + script_name: scriptName, + class_name: workflow.class_name, + }), + headers: { + "Content-Type": "application/json", + }, + }).then(() => [`workflow: ${workflow.name}`]) + ); + } + } + const targets = await Promise.all(deployments); const deployMs = Date.now() - start - uploadMs; diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index 944fc9f3f377..35779b27660d 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -298,6 +298,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m moduleCollector, serveLegacyAssetsFromWorker: false, doBindings: config.durable_objects.bindings, + workflowBindings: config.workflows, jsxFactory, jsxFragment, tsconfig: props.tsconfig ?? config.tsconfig, @@ -367,6 +368,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m text_blobs: config.text_blobs, data_blobs: config.data_blobs, durable_objects: config.durable_objects, + workflows: config.workflows, queues: config.queues.producers?.map((producer) => { return { binding: producer.binding, queue_name: producer.queue }; }), diff --git a/packages/wrangler/src/workflows/commands/delete.ts b/packages/wrangler/src/workflows/commands/delete.ts new file mode 100644 index 000000000000..53cf826f14f7 --- /dev/null +++ b/packages/wrangler/src/workflows/commands/delete.ts @@ -0,0 +1,33 @@ +import { fetchResult } from "../../cfetch"; +import { readConfig } from "../../config"; +import { logger } from "../../logger"; +import { printWranglerBanner } from "../../update-check"; +import { requireAuth } from "../../user"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../yargs-types"; + +export const workflowDeleteOptions = (args: CommonYargsArgv) => { + return args.positional("name", { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface< + typeof workflowDeleteOptions +>; +export const workflowDeleteHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + await fetchResult(`/accounts/${accountId}/workflows/${args.name}`, { + method: "DELETE", + }); + + logger.info(`Workflow "${args.name}" was successfully removed`); +}; diff --git a/packages/wrangler/src/workflows/commands/describe.ts b/packages/wrangler/src/workflows/commands/describe.ts new file mode 100644 index 000000000000..36b7ef6d62d4 --- /dev/null +++ b/packages/wrangler/src/workflows/commands/describe.ts @@ -0,0 +1,62 @@ +import { logRaw } from "@cloudflare/cli"; +import { white } from "@cloudflare/cli/colors"; +import { fetchResult } from "../../cfetch"; +import { readConfig } from "../../config"; +import { printWranglerBanner } from "../../update-check"; +import { requireAuth } from "../../user"; +import formatLabelledValues from "../../utils/render-labelled-values"; +import { + type CommonYargsArgv, + type StrictYargsOptionsToInterface, +} from "../../yargs-types"; +import type { Version, Workflow } from "../types"; + +export const workflowDescribeOptions = (args: CommonYargsArgv) => { + return args.positional("name", { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface< + typeof workflowDescribeOptions +>; +export const workflowDescribeHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + const workflow = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}` + ); + + const versions = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/versions` + ); + + const latestVersion = versions[0]; + + logRaw( + formatLabelledValues({ + Name: workflow.name, + Id: workflow.id, + "Script Name": workflow.script_name, + "Class Name": workflow.class_name, + "Created On": workflow.created_on, + "Modified On": workflow.modified_on, + }) + ); + logRaw(white("Latest Version:")); + logRaw( + formatLabelledValues( + { + Id: latestVersion.id, + "Created On": workflow.created_on, + "Modified On": workflow.modified_on, + }, + { indentationCount: 2 } + ) + ); +}; diff --git a/packages/wrangler/src/workflows/commands/instances/describe.ts b/packages/wrangler/src/workflows/commands/instances/describe.ts new file mode 100644 index 000000000000..f227e941d9fe --- /dev/null +++ b/packages/wrangler/src/workflows/commands/instances/describe.ts @@ -0,0 +1,280 @@ +import { logRaw } from "@cloudflare/cli"; +import { red, white } from "@cloudflare/cli/colors"; +import { + addMilliseconds, + formatDistanceStrict, + formatDistanceToNowStrict, +} from "date-fns"; +import { ms } from "itty-time"; +import { fetchResult } from "../../../cfetch"; +import { readConfig } from "../../../config"; +import { logger } from "../../../logger"; +import { printWranglerBanner } from "../../../update-check"; +import { requireAuth } from "../../../user"; +import formatLabelledValues from "../../../utils/render-labelled-values"; +import { + emojifyInstanceStatus, + emojifyInstanceTriggerName, + emojifyStepType, +} from "../../utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../../yargs-types"; +import type { + Instance, + InstanceSleepLog, + InstanceStatusAndLogs, + InstanceStepLog, + InstanceTerminateLog, +} from "../../types"; + +export const instancesDescribeOptions = (args: CommonYargsArgv) => { + return args + .positional("name", { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }) + .positional("id", { + describe: + "ID of the instance - instead of an UUID you can type 'latest' to get the latest instance and describe it", + type: "string", + demandOption: true, + }) + .option("step-output", { + describe: + "Don't output the step output since it might clutter the terminal", + type: "boolean", + default: true, + }) + .option("truncate-output-limit", { + describe: "Truncate step output after x characters", + type: "number", + default: 5000, + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface< + typeof instancesDescribeOptions +>; + +export const instancesDescribeHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + let id = args.id; + + if (id == "latest") { + const instances = ( + await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances` + ) + ).sort((a, b) => b.created_on.localeCompare(a.created_on)); + + if (instances.length == 0) { + logger.error( + `There are no deployed instances in workflow "${args.name}".` + ); + return; + } + + id = instances[0].id; + } + + const instance = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances/${id}` + ); + + const formattedInstance: Record = { + "Workflow Name": args.name, + "Instance Id": id, + "Version Id": instance.versionId, + Status: emojifyInstanceStatus(instance.status), + Trigger: emojifyInstanceTriggerName(instance.trigger.source), + Queued: new Date(instance.queued).toLocaleString(), + }; + + if (instance.success != null) { + formattedInstance.Success = instance.success ? "✅ Yes" : "❌ No"; + } + + // date related stuff, if the workflow is still running assume duration until now + if (instance.start != undefined) { + formattedInstance.Start = new Date(instance.start).toLocaleString(); + } + + if (instance.end != undefined) { + formattedInstance.End = new Date(instance.end).toLocaleString(); + } + + if (instance.start != null && instance.end != null) { + formattedInstance.Duration = formatDistanceStrict( + new Date(instance.end), + new Date(instance.start) + ); + } else if (instance.start != null) { + // Convert current date to UTC + formattedInstance.Duration = formatDistanceStrict( + new Date(instance.start), + new Date(new Date().toUTCString().slice(0, -4)) + ); + } + + const lastSuccessfulStepName = getLastSuccessfulStep(instance); + if (lastSuccessfulStepName != null) { + formattedInstance["Last Successful Step"] = lastSuccessfulStepName; + } + + // display the error if the instance errored out + if (instance.error != null) { + formattedInstance.Error = red( + `${instance.error.name}: ${instance.error.message}` + ); + } + + logRaw(formatLabelledValues(formattedInstance)); + logRaw(white("Steps:")); + + instance.steps.forEach(logStep.bind(false, args)); +}; + +const logStep = ( + args: HandlerOptions, + step: InstanceStepLog | InstanceSleepLog | InstanceTerminateLog +) => { + logRaw(""); + const formattedStep: Record = {}; + + if (step.type == "sleep" || step.type == "step") { + formattedStep.Name = step.name; + formattedStep.Type = emojifyStepType(step.type); + + // date related stuff, if the step is still running assume duration until now + if (step.start != undefined) { + formattedStep.Start = new Date(step.start).toLocaleString(); + } + + if (step.end != undefined) { + formattedStep.End = new Date(step.end).toLocaleString(); + } + + if (step.start != null && step.end != null) { + formattedStep.Duration = formatDistanceStrict( + new Date(step.end), + new Date(step.start) + ); + } else if (step.start != null) { + // Convert current date to UTC + formattedStep.Duration = formatDistanceStrict( + new Date(step.start), + new Date(new Date().toUTCString().slice(0, -4)) + ); + } + } else if (step.type == "termination") { + formattedStep.Type = emojifyStepType(step.type); + formattedStep.Trigger = step.trigger.source; + } + + if (step.type == "step") { + if (step.success !== null) { + formattedStep.Success = step.success ? "✅ Yes" : "❌ No"; + } else { + formattedStep.Success = "▶ Running"; + } + + if (step.success === null) { + const latestAttempt = step.attempts.at(-1); + let delay = step.config.retries.delay; + if (latestAttempt !== undefined && latestAttempt.success === false) { + // SAFETY: It's okay because end date must always exist in the API, otherwise it's okay to fail + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const endDate = new Date(latestAttempt.end!); + if (typeof delay === "string") { + delay = ms(delay); + } + const retryDate = addMilliseconds(endDate, delay); + formattedStep["Retries At"] = + `${retryDate.toLocaleString()} (in ${formatDistanceToNowStrict(retryDate)} from now)`; + } + } + if (step.output !== undefined && args.stepOutput) { + let output: string; + try { + output = JSON.stringify(step.output); + } catch { + output = step.output as string; + } + formattedStep.Output = + output.length > args.truncateOutputLimit + ? output.substring(0, args.truncateOutputLimit) + + "[...output truncated]" + : output; + } + } + + logRaw(formatLabelledValues(formattedStep, { indentationCount: 2 })); + + if (step.type == "step") { + const prettyAttempts = step.attempts.map((val) => { + const attempt: Record = {}; + + attempt.Start = new Date(val.start).toLocaleString(); + attempt.End = val.end == null ? "" : new Date(val.end).toLocaleString(); + + if (val.start != null && val.end != null) { + attempt.Duration = formatDistanceStrict( + new Date(val.end), + new Date(val.start) + ); + } else if (val.start != null) { + // Converting datetimes into UTC is very cool in JS + attempt.Duration = formatDistanceStrict( + new Date(val.start), + new Date(new Date().toUTCString().slice(0, -4)) + ); + } + + attempt.State = + val.success == null + ? "🔄 Working" + : val.success + ? "✅ Success" + : "❌ Error"; + + // This is actually safe to do while logger.table only considers the first element as keys. + // Because if there's an error, the first row will always be an error + if (val.error != null) { + attempt.Error = red(`${val.error.name}: ${val.error.message}`); + } + return attempt; + }); + + logger.table(prettyAttempts); + } +}; + +const getLastSuccessfulStep = (logs: InstanceStatusAndLogs): string | null => { + let lastSuccessfulStepName: string | null = null; + + for (const step of logs.steps) { + switch (step.type) { + case "step": + if (step.success == true) { + lastSuccessfulStepName = step.name; + } + break; + case "sleep": + if (step.end != null) { + lastSuccessfulStepName = step.name; + } + break; + case "termination": + break; + } + } + + return lastSuccessfulStepName; +}; diff --git a/packages/wrangler/src/workflows/commands/instances/index.ts b/packages/wrangler/src/workflows/commands/instances/index.ts new file mode 100644 index 000000000000..51e29a8c03db --- /dev/null +++ b/packages/wrangler/src/workflows/commands/instances/index.ts @@ -0,0 +1,29 @@ +import { instancesDescribeHandler, instancesDescribeOptions } from "./describe"; +import { instancesListHandler, instancesListOptions } from "./list"; +import { + instancesTerminateHandler, + instancesTerminateOptions, +} from "./terminate"; +import type { CommonYargsArgv } from "../../../yargs-types"; + +export const instances = (args: CommonYargsArgv) => { + return args + .command( + "list ", + "List workflow instances", + instancesListOptions, + instancesListHandler + ) + .command( + "describe ", + "Describe a workflow instance - see its logs, retries and errors", + instancesDescribeOptions, + instancesDescribeHandler + ) + .command( + "terminate ", + "Terminate a workflow instance", + instancesTerminateOptions, + instancesTerminateHandler + ); +}; diff --git a/packages/wrangler/src/workflows/commands/instances/list.ts b/packages/wrangler/src/workflows/commands/instances/list.ts new file mode 100644 index 000000000000..652402a75def --- /dev/null +++ b/packages/wrangler/src/workflows/commands/instances/list.ts @@ -0,0 +1,95 @@ +import { printWranglerBanner } from "../../.."; +import { fetchResult } from "../../../cfetch"; +import { readConfig } from "../../../config"; +import { logger } from "../../../logger"; +import { requireAuth } from "../../../user"; +import { emojifyInstanceStatus, validateStatus } from "../../utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../../yargs-types"; +import type { Instance } from "../../types"; + +export const instancesListOptions = (args: CommonYargsArgv) => { + return args + .positional("name", { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }) + .option("reverse", { + describe: "Reverse order of the instances table", + type: "boolean", + default: false, + }) + .option("status", { + describe: + "Filters list by instance status (can be one of: queued, running, paused, errored, terminated, complete)", + type: "string", + }) + .option("page", { + describe: + 'Show a sepecific page from the listing, can configure page size using "per-page".', + type: "number", + default: 1, + }) + .option("per-page", { + describe: "Configure the maximum number of instances to show per page.", + type: "number", + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface< + typeof instancesListOptions +>; +export const instancesListHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + const URLParams = new URLSearchParams(); + + if (args.status !== undefined) { + const validatedStatus = validateStatus(args.status); + URLParams.set("status", validatedStatus); + } + if (args.perPage !== undefined) { + URLParams.set("per_page", args.perPage.toString()); + } + + URLParams.set("page", args.page.toString()); + + const instances = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances`, + undefined, + URLParams + ); + + if (instances.length === 0) { + logger.warn( + `There are no instances in workflow "${args.name}". You can trigger it with "wrangler workflows trigger ${args.name}"` + ); + return; + } + + logger.info( + `Showing ${instances.length} instance${instances.length > 1 ? "s" : ""} from page ${args.page}:` + ); + + const prettierInstances = instances + .sort((a, b) => + args.reverse + ? a.modified_on.localeCompare(b.modified_on) + : b.modified_on.localeCompare(a.modified_on) + ) + .map((instance) => ({ + Id: instance.id, + Version: instance.version_id, + Created: new Date(instance.created_on).toLocaleString(), + Modified: new Date(instance.modified_on).toLocaleString(), + Status: emojifyInstanceStatus(instance.status), + })); + + logger.table(prettierInstances); +}; diff --git a/packages/wrangler/src/workflows/commands/instances/terminate.ts b/packages/wrangler/src/workflows/commands/instances/terminate.ts new file mode 100644 index 000000000000..543d182b5780 --- /dev/null +++ b/packages/wrangler/src/workflows/commands/instances/terminate.ts @@ -0,0 +1,66 @@ +import { fetchResult } from "../../../cfetch"; +import { readConfig } from "../../../config"; +import { logger } from "../../../logger"; +import { printWranglerBanner } from "../../../update-check"; +import { requireAuth } from "../../../user"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../../yargs-types"; +import type { Instance } from "../../types"; + +export const instancesTerminateOptions = (args: CommonYargsArgv) => { + return args + .positional("name", { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }) + .positional("id", { + describe: + "ID of the instance - instead of an UUID you can type 'latest' to get the latest instance and describe it", + type: "string", + demandOption: true, + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface< + typeof instancesTerminateOptions +>; + +export const instancesTerminateHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + let id = args.id; + + if (id == "latest") { + const instances = ( + await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances` + ) + ).sort((a, b) => b.created_on.localeCompare(a.created_on)); + + if (instances.length == 0) { + logger.error( + `There are no deployed instances in workflow "${args.name}".` + ); + return; + } + + id = instances[0].id; + } + + await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances/${id}`, + { + method: "DELETE", + } + ); + + logger.info( + `🥷 The instance "${id}" from ${args.name} was terminated successfully.` + ); +}; diff --git a/packages/wrangler/src/workflows/commands/list.ts b/packages/wrangler/src/workflows/commands/list.ts new file mode 100644 index 000000000000..13d3ca1961fe --- /dev/null +++ b/packages/wrangler/src/workflows/commands/list.ts @@ -0,0 +1,66 @@ +import { printWranglerBanner } from "../.."; +import { fetchResult } from "../../cfetch"; +import { readConfig } from "../../config"; +import { logger } from "../../logger"; +import { requireAuth } from "../../user"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../yargs-types"; +import type { Workflow } from "../types"; + +export const workflowListOptions = (args: CommonYargsArgv) => { + return args + .option("page", { + describe: + 'Show a sepecific page from the listing, can configure page size using "per-page".', + type: "number", + default: 1, + }) + .option("per-page", { + describe: "Configure the maximum number of workflows to show per page.", + type: "number", + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface; +export const workflowListHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + const URLParams = new URLSearchParams(); + + if (args.perPage !== undefined) { + URLParams.set("per_page", args.perPage.toString()); + } + + URLParams.set("page", args.page.toString()); + + const workflows = await fetchResult( + `/accounts/${accountId}/workflows`, + undefined, + URLParams + ); + + if (workflows.length === 0) { + logger.warn("There are no deployed Workflows in this account."); + } else { + // TODO(lduarte): can we improve this message once pagination is deployed + logger.info( + `Showing last ${workflows.length} workflow${workflows.length > 1 ? "s" : ""}:` + ); + // sort by name and make the table head prettier by changing the keys + const prettierWorkflows = workflows + .sort((a, b) => a.name.localeCompare(b.name)) + .map((workflow) => ({ + Name: workflow.name, + "Script name": workflow.script_name, + "Class name": workflow.class_name, + Created: new Date(workflow.created_on).toLocaleString(), + Modified: new Date(workflow.modified_on).toLocaleString(), + })); + logger.table(prettierWorkflows); + } +}; diff --git a/packages/wrangler/src/workflows/commands/trigger.ts b/packages/wrangler/src/workflows/commands/trigger.ts new file mode 100644 index 000000000000..aa087a9fc80f --- /dev/null +++ b/packages/wrangler/src/workflows/commands/trigger.ts @@ -0,0 +1,57 @@ +import { fetchResult } from "../../cfetch"; +import { readConfig } from "../../config"; +import { logger } from "../../logger"; +import { printWranglerBanner } from "../../update-check"; +import { requireAuth } from "../../user"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../yargs-types"; +import type { InstanceWithoutDates } from "../types"; + +export const workflowTriggerOptions = (args: CommonYargsArgv) => { + return args + .positional("name", { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }) + .positional("params", { + describe: "Params for the workflow instance, encoded as a JSON string", + type: "string", + default: "", + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface< + typeof workflowTriggerOptions +>; +export const workflowTriggerHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + if (args.params.length != 0) { + try { + JSON.parse(args.params); + } catch (e) { + logger.error( + `Error while parsing instance parameters: "${args.params}" with ${e}' ` + ); + return; + } + } + + const response = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances`, + { + method: "POST", + body: args.params.length != 0 ? args.params : undefined, + } + ); + + logger.info( + `🚀 Workflow instance "${response.id}" has been queued successfully` + ); +}; diff --git a/packages/wrangler/src/workflows/types.ts b/packages/wrangler/src/workflows/types.ts new file mode 100644 index 000000000000..e41221595889 --- /dev/null +++ b/packages/wrangler/src/workflows/types.ts @@ -0,0 +1,106 @@ +export type Workflow = { + name: string; + id: string; + created_on: string; + modified_on: string; + script_name: string; + class_name: string; +}; + +export type Version = { + id: string; + created_on: string; + modified_on: string; + workflow_id: string; +}; + +export type InstanceStatus = + | "unknown" + | "queued" + | "running" + | "paused" + | "errored" + | "terminated" + | "complete"; + +export type InstanceWithoutDates = { + status: InstanceStatus; + id: string; + version_id: string; + workflow_id: string; +}; + +export type Instance = { + id: string; + created_on: string; + modified_on: string; + workflow_id: string; + version_id: string; + status: InstanceStatus; +}; + +export type InstanceTriggerName = + | "api" + | "binding" + | "event" + | "cron" + | "unknown"; + +export type InstanceAttempt = { + start: string; + end: string | null; + success: boolean | null; + error: { name: string; message: string } | null; +}; + +export type Backoff = "constant" | "linear" | "exponential"; + +export type StepConfig = { + retries: { + limit: number; + delay: string | number; + backoff?: Backoff; + }; + timeout: string | number; +}; + +export type InstanceStepLog = { + name: string; + start: string; + end: string | null; + attempts: InstanceAttempt[]; + output: unknown; + success: boolean | null; + config: StepConfig; + type: "step"; +}; + +export type InstanceSleepLog = { + name: string; + start: string; + end: string; + finished: boolean; + type: "sleep"; +}; + +export type InstanceTerminateLog = { + type: "termination"; + trigger: { + source: string; + }; +}; + +export type InstanceStatusAndLogs = { + status: InstanceStatus; + params: Record; + trigger: { + source: InstanceTriggerName; + }; + versionId: string; + queued: string; + start: string | null; + end: string | null; + steps: (InstanceStepLog | InstanceSleepLog | InstanceTerminateLog)[]; + success: boolean | null; + error: { name: string; message: string } | null; +}; diff --git a/packages/wrangler/src/workflows/utils.ts b/packages/wrangler/src/workflows/utils.ts new file mode 100644 index 000000000000..f4d119041af3 --- /dev/null +++ b/packages/wrangler/src/workflows/utils.ts @@ -0,0 +1,70 @@ +import { UserError } from "../errors"; +import type { InstanceStatus, InstanceTriggerName } from "./types"; + +export const emojifyInstanceStatus = (status: InstanceStatus) => { + switch (status) { + case "complete": + return "✅ Completed"; + case "errored": + return "❌ Errored"; + case "unknown": + return "❓ Unknown"; + case "paused": + return "⏸️ Paused"; + case "queued": + return "⌛ Queued"; + case "running": + return "▶ Running"; + case "terminated": + return "🚫 Terminated"; + } +}; + +export const emojifyInstanceTriggerName = (status: InstanceTriggerName) => { + switch (status) { + case "api": + return "🌎 API"; + case "binding": + return "🔗 Binding"; + case "cron": + return "⌛ Cron"; + case "event": + return "📩 Event"; + default: + return "❓ Unknown"; + } +}; + +export const emojifyStepType = (type: string) => { + switch (type) { + case "step": + return "🎯 Step"; + case "sleep": + return "💤 Sleeping"; + case "termination": + return "🚫 Termination"; + default: + return "❓ Unknown"; + } +}; + +export const validateStatus = (status: string): InstanceStatus => { + switch (status) { + case "complete": + return "complete"; + case "errored": + return "errored"; + case "paused": + return "paused"; + case "queued": + return "queued"; + case "running": + return "running"; + case "terminated": + return "terminated"; + default: + throw new UserError( + `Looks like you have provided a invalid status "${status}". Valid statuses are: queued, running, paused, errored, terminated, complete` + ); + } +}; diff --git a/packages/wrangler/src/workflows/workflows.ts b/packages/wrangler/src/workflows/workflows.ts new file mode 100644 index 000000000000..5d086a8b42ed --- /dev/null +++ b/packages/wrangler/src/workflows/workflows.ts @@ -0,0 +1,54 @@ +import { type CommonYargsArgv, type SubHelp } from "../yargs-types"; +import { + workflowDeleteHandler, + workflowDeleteOptions, +} from "./commands/delete"; +import { + workflowDescribeHandler, + workflowDescribeOptions, +} from "./commands/describe"; +import { instances } from "./commands/instances"; +import { workflowListHandler, workflowListOptions } from "./commands/list"; +import { + workflowTriggerHandler, + workflowTriggerOptions, +} from "./commands/trigger"; + +const workflowsEpilog = + "🚨 'wrangler workflows ...' commands are currently in private beta. If your account isn't authorized, your commands will fail."; + +export const workflows = (yargs: CommonYargsArgv, subHelp: SubHelp) => { + return yargs + .command( + "list", + "List Workflows associated to account", + workflowListOptions, + workflowListHandler + ) + .command( + "describe ", + "Describe Workflow resource", + workflowDescribeOptions, + workflowDescribeHandler + ) + .command( + "delete ", + "Delete workflow - when deleting a workflow, it will also delete it's own instances", + workflowDeleteOptions, + workflowDeleteHandler + ) + .command( + "trigger [params]", + "Trigger a workflow, creating a new instance. Can optionally take a JSON string to pass a parameter into the workflow instance.", + workflowTriggerOptions, + workflowTriggerHandler + ) + .command( + "instances", + "Instance related commands (list, describe, terminate...)", + (instancesYargs) => { + return instances(instancesYargs).command(subHelp); + } + ) + .epilog(workflowsEpilog); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1c9f6015b05..20cf9d62f28f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,12 @@ settings: catalogs: default: + '@vitest/runner': + specifier: ~2.1.1 + version: 2.1.1 + '@vitest/snapshot': + specifier: ~2.1.1 + version: 2.1.1 vitest: specifier: ~2.1.1 version: 2.1.1 @@ -732,6 +738,15 @@ importers: specifier: workspace:* version: link:../../packages/wrangler + fixtures/workflow: + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20240909.0 + version: 4.20241011.0 + wrangler: + specifier: workspace:* + version: link:../../packages/wrangler + packages/cli: devDependencies: '@clack/core': @@ -1632,9 +1647,15 @@ importers: chokidar: specifier: ^3.5.3 version: 3.5.3 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 esbuild: specifier: 0.17.19 version: 0.17.19 + itty-time: + specifier: ^1.0.6 + version: 1.0.6 miniflare: specifier: workspace:* version: link:../miniflare @@ -4578,6 +4599,9 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + date-time@3.1.0: resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} engines: {node: '>=6'} @@ -5903,6 +5927,9 @@ packages: itty-router@4.0.17: resolution: {integrity: sha512-Fu/GO3MFX6Hwd+QF1/BFjoFpPQKh2Bu4DtBdse5kECcUwldNBrZqgwq0IariLrP67iADGlzkHIlOWwJ8F4SJ4A==} + itty-time@1.0.6: + resolution: {integrity: sha512-+P8IZaLLBtFv8hCkIjcymZOp4UJ+xW6bSlQsXGqrkmJh7vSiMFSlNne0mCYagEE0N7HDNR5jJBRxwN0oYv61Rw==} + jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} @@ -7761,8 +7788,8 @@ packages: resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + tinyspy@3.0.0: + resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} engines: {node: '>=14.0.0'} title-case@2.1.1: @@ -10569,7 +10596,7 @@ snapshots: '@vitest/spy@2.1.1': dependencies: - tinyspy: 3.0.2 + tinyspy: 3.0.0 '@vitest/ui@1.6.0(vitest@2.1.1)': dependencies: @@ -11533,6 +11560,8 @@ snapshots: dependencies: '@babel/runtime': 7.22.5 + date-fns@4.1.0: {} + date-time@3.1.0: dependencies: time-zone: 1.0.0 @@ -13098,6 +13127,8 @@ snapshots: itty-router@4.0.17: {} + itty-time@1.0.6: {} + jackspeak@2.3.6: dependencies: '@isaacs/cliui': 8.0.2 @@ -15023,7 +15054,7 @@ snapshots: tinyrainbow@1.2.0: {} - tinyspy@3.0.2: {} + tinyspy@3.0.0: {} title-case@2.1.1: dependencies: From 0cb8b8ca4cdaa5aedbd99564c21fadb2ee61bcc8 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Thu, 17 Oct 2024 21:48:45 +0100 Subject: [PATCH 02/16] remove commands for another PR --- packages/wrangler/package.json | 2 - packages/wrangler/src/index.ts | 6 - .../wrangler/src/workflows/commands/delete.ts | 33 --- .../src/workflows/commands/describe.ts | 62 ---- .../workflows/commands/instances/describe.ts | 280 ------------------ .../src/workflows/commands/instances/index.ts | 29 -- .../src/workflows/commands/instances/list.ts | 95 ------ .../workflows/commands/instances/terminate.ts | 66 ----- .../wrangler/src/workflows/commands/list.ts | 66 ----- .../src/workflows/commands/trigger.ts | 57 ---- packages/wrangler/src/workflows/utils.ts | 70 ----- packages/wrangler/src/workflows/workflows.ts | 54 ---- pnpm-lock.yaml | 16 - 13 files changed, 836 deletions(-) delete mode 100644 packages/wrangler/src/workflows/commands/delete.ts delete mode 100644 packages/wrangler/src/workflows/commands/describe.ts delete mode 100644 packages/wrangler/src/workflows/commands/instances/describe.ts delete mode 100644 packages/wrangler/src/workflows/commands/instances/index.ts delete mode 100644 packages/wrangler/src/workflows/commands/instances/list.ts delete mode 100644 packages/wrangler/src/workflows/commands/instances/terminate.ts delete mode 100644 packages/wrangler/src/workflows/commands/list.ts delete mode 100644 packages/wrangler/src/workflows/commands/trigger.ts delete mode 100644 packages/wrangler/src/workflows/utils.ts delete mode 100644 packages/wrangler/src/workflows/workflows.ts diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 2ffedcf65f49..27151af3d295 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -75,9 +75,7 @@ "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", "chokidar": "^3.5.3", - "date-fns": "^4.1.0", "esbuild": "0.17.19", - "itty-time": "^1.0.6", "miniflare": "workspace:*", "nanoid": "^3.3.3", "path-to-regexp": "^6.3.0", diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index 0df0acca9384..b6b1422beed3 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -77,7 +77,6 @@ import registerVersionsSubcommands from "./versions"; import registerVersionsDeploymentsSubcommands from "./versions/deployments"; import registerVersionsRollbackCommand from "./versions/rollback"; import { whoami } from "./whoami"; -import { workflows } from "./workflows/workflows"; import { asJson } from "./yargs-types"; import type { Config } from "./config"; import type { LoggerLevel } from "./logger"; @@ -609,11 +608,6 @@ export function createCLIParser(argv: string[]) { return ai(aiYargs.command(subHelp)); }); - // workflows - wrangler.command("workflows", false, (workflowArgs) => { - return workflows(workflowArgs.command(subHelp), subHelp); - }); - // pipelines wrangler.command("pipelines", false, (pipelinesYargs) => { return pipelines(pipelinesYargs.command(subHelp)); diff --git a/packages/wrangler/src/workflows/commands/delete.ts b/packages/wrangler/src/workflows/commands/delete.ts deleted file mode 100644 index 53cf826f14f7..000000000000 --- a/packages/wrangler/src/workflows/commands/delete.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { fetchResult } from "../../cfetch"; -import { readConfig } from "../../config"; -import { logger } from "../../logger"; -import { printWranglerBanner } from "../../update-check"; -import { requireAuth } from "../../user"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; - -export const workflowDeleteOptions = (args: CommonYargsArgv) => { - return args.positional("name", { - describe: "Name of the workflow", - type: "string", - demandOption: true, - }); -}; - -type HandlerOptions = StrictYargsOptionsToInterface< - typeof workflowDeleteOptions ->; -export const workflowDeleteHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); - - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); - - await fetchResult(`/accounts/${accountId}/workflows/${args.name}`, { - method: "DELETE", - }); - - logger.info(`Workflow "${args.name}" was successfully removed`); -}; diff --git a/packages/wrangler/src/workflows/commands/describe.ts b/packages/wrangler/src/workflows/commands/describe.ts deleted file mode 100644 index 36b7ef6d62d4..000000000000 --- a/packages/wrangler/src/workflows/commands/describe.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { logRaw } from "@cloudflare/cli"; -import { white } from "@cloudflare/cli/colors"; -import { fetchResult } from "../../cfetch"; -import { readConfig } from "../../config"; -import { printWranglerBanner } from "../../update-check"; -import { requireAuth } from "../../user"; -import formatLabelledValues from "../../utils/render-labelled-values"; -import { - type CommonYargsArgv, - type StrictYargsOptionsToInterface, -} from "../../yargs-types"; -import type { Version, Workflow } from "../types"; - -export const workflowDescribeOptions = (args: CommonYargsArgv) => { - return args.positional("name", { - describe: "Name of the workflow", - type: "string", - demandOption: true, - }); -}; - -type HandlerOptions = StrictYargsOptionsToInterface< - typeof workflowDescribeOptions ->; -export const workflowDescribeHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); - - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); - - const workflow = await fetchResult( - `/accounts/${accountId}/workflows/${args.name}` - ); - - const versions = await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/versions` - ); - - const latestVersion = versions[0]; - - logRaw( - formatLabelledValues({ - Name: workflow.name, - Id: workflow.id, - "Script Name": workflow.script_name, - "Class Name": workflow.class_name, - "Created On": workflow.created_on, - "Modified On": workflow.modified_on, - }) - ); - logRaw(white("Latest Version:")); - logRaw( - formatLabelledValues( - { - Id: latestVersion.id, - "Created On": workflow.created_on, - "Modified On": workflow.modified_on, - }, - { indentationCount: 2 } - ) - ); -}; diff --git a/packages/wrangler/src/workflows/commands/instances/describe.ts b/packages/wrangler/src/workflows/commands/instances/describe.ts deleted file mode 100644 index f227e941d9fe..000000000000 --- a/packages/wrangler/src/workflows/commands/instances/describe.ts +++ /dev/null @@ -1,280 +0,0 @@ -import { logRaw } from "@cloudflare/cli"; -import { red, white } from "@cloudflare/cli/colors"; -import { - addMilliseconds, - formatDistanceStrict, - formatDistanceToNowStrict, -} from "date-fns"; -import { ms } from "itty-time"; -import { fetchResult } from "../../../cfetch"; -import { readConfig } from "../../../config"; -import { logger } from "../../../logger"; -import { printWranglerBanner } from "../../../update-check"; -import { requireAuth } from "../../../user"; -import formatLabelledValues from "../../../utils/render-labelled-values"; -import { - emojifyInstanceStatus, - emojifyInstanceTriggerName, - emojifyStepType, -} from "../../utils"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../../yargs-types"; -import type { - Instance, - InstanceSleepLog, - InstanceStatusAndLogs, - InstanceStepLog, - InstanceTerminateLog, -} from "../../types"; - -export const instancesDescribeOptions = (args: CommonYargsArgv) => { - return args - .positional("name", { - describe: "Name of the workflow", - type: "string", - demandOption: true, - }) - .positional("id", { - describe: - "ID of the instance - instead of an UUID you can type 'latest' to get the latest instance and describe it", - type: "string", - demandOption: true, - }) - .option("step-output", { - describe: - "Don't output the step output since it might clutter the terminal", - type: "boolean", - default: true, - }) - .option("truncate-output-limit", { - describe: "Truncate step output after x characters", - type: "number", - default: 5000, - }); -}; - -type HandlerOptions = StrictYargsOptionsToInterface< - typeof instancesDescribeOptions ->; - -export const instancesDescribeHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); - - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); - - let id = args.id; - - if (id == "latest") { - const instances = ( - await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/instances` - ) - ).sort((a, b) => b.created_on.localeCompare(a.created_on)); - - if (instances.length == 0) { - logger.error( - `There are no deployed instances in workflow "${args.name}".` - ); - return; - } - - id = instances[0].id; - } - - const instance = await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/instances/${id}` - ); - - const formattedInstance: Record = { - "Workflow Name": args.name, - "Instance Id": id, - "Version Id": instance.versionId, - Status: emojifyInstanceStatus(instance.status), - Trigger: emojifyInstanceTriggerName(instance.trigger.source), - Queued: new Date(instance.queued).toLocaleString(), - }; - - if (instance.success != null) { - formattedInstance.Success = instance.success ? "✅ Yes" : "❌ No"; - } - - // date related stuff, if the workflow is still running assume duration until now - if (instance.start != undefined) { - formattedInstance.Start = new Date(instance.start).toLocaleString(); - } - - if (instance.end != undefined) { - formattedInstance.End = new Date(instance.end).toLocaleString(); - } - - if (instance.start != null && instance.end != null) { - formattedInstance.Duration = formatDistanceStrict( - new Date(instance.end), - new Date(instance.start) - ); - } else if (instance.start != null) { - // Convert current date to UTC - formattedInstance.Duration = formatDistanceStrict( - new Date(instance.start), - new Date(new Date().toUTCString().slice(0, -4)) - ); - } - - const lastSuccessfulStepName = getLastSuccessfulStep(instance); - if (lastSuccessfulStepName != null) { - formattedInstance["Last Successful Step"] = lastSuccessfulStepName; - } - - // display the error if the instance errored out - if (instance.error != null) { - formattedInstance.Error = red( - `${instance.error.name}: ${instance.error.message}` - ); - } - - logRaw(formatLabelledValues(formattedInstance)); - logRaw(white("Steps:")); - - instance.steps.forEach(logStep.bind(false, args)); -}; - -const logStep = ( - args: HandlerOptions, - step: InstanceStepLog | InstanceSleepLog | InstanceTerminateLog -) => { - logRaw(""); - const formattedStep: Record = {}; - - if (step.type == "sleep" || step.type == "step") { - formattedStep.Name = step.name; - formattedStep.Type = emojifyStepType(step.type); - - // date related stuff, if the step is still running assume duration until now - if (step.start != undefined) { - formattedStep.Start = new Date(step.start).toLocaleString(); - } - - if (step.end != undefined) { - formattedStep.End = new Date(step.end).toLocaleString(); - } - - if (step.start != null && step.end != null) { - formattedStep.Duration = formatDistanceStrict( - new Date(step.end), - new Date(step.start) - ); - } else if (step.start != null) { - // Convert current date to UTC - formattedStep.Duration = formatDistanceStrict( - new Date(step.start), - new Date(new Date().toUTCString().slice(0, -4)) - ); - } - } else if (step.type == "termination") { - formattedStep.Type = emojifyStepType(step.type); - formattedStep.Trigger = step.trigger.source; - } - - if (step.type == "step") { - if (step.success !== null) { - formattedStep.Success = step.success ? "✅ Yes" : "❌ No"; - } else { - formattedStep.Success = "▶ Running"; - } - - if (step.success === null) { - const latestAttempt = step.attempts.at(-1); - let delay = step.config.retries.delay; - if (latestAttempt !== undefined && latestAttempt.success === false) { - // SAFETY: It's okay because end date must always exist in the API, otherwise it's okay to fail - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const endDate = new Date(latestAttempt.end!); - if (typeof delay === "string") { - delay = ms(delay); - } - const retryDate = addMilliseconds(endDate, delay); - formattedStep["Retries At"] = - `${retryDate.toLocaleString()} (in ${formatDistanceToNowStrict(retryDate)} from now)`; - } - } - if (step.output !== undefined && args.stepOutput) { - let output: string; - try { - output = JSON.stringify(step.output); - } catch { - output = step.output as string; - } - formattedStep.Output = - output.length > args.truncateOutputLimit - ? output.substring(0, args.truncateOutputLimit) + - "[...output truncated]" - : output; - } - } - - logRaw(formatLabelledValues(formattedStep, { indentationCount: 2 })); - - if (step.type == "step") { - const prettyAttempts = step.attempts.map((val) => { - const attempt: Record = {}; - - attempt.Start = new Date(val.start).toLocaleString(); - attempt.End = val.end == null ? "" : new Date(val.end).toLocaleString(); - - if (val.start != null && val.end != null) { - attempt.Duration = formatDistanceStrict( - new Date(val.end), - new Date(val.start) - ); - } else if (val.start != null) { - // Converting datetimes into UTC is very cool in JS - attempt.Duration = formatDistanceStrict( - new Date(val.start), - new Date(new Date().toUTCString().slice(0, -4)) - ); - } - - attempt.State = - val.success == null - ? "🔄 Working" - : val.success - ? "✅ Success" - : "❌ Error"; - - // This is actually safe to do while logger.table only considers the first element as keys. - // Because if there's an error, the first row will always be an error - if (val.error != null) { - attempt.Error = red(`${val.error.name}: ${val.error.message}`); - } - return attempt; - }); - - logger.table(prettyAttempts); - } -}; - -const getLastSuccessfulStep = (logs: InstanceStatusAndLogs): string | null => { - let lastSuccessfulStepName: string | null = null; - - for (const step of logs.steps) { - switch (step.type) { - case "step": - if (step.success == true) { - lastSuccessfulStepName = step.name; - } - break; - case "sleep": - if (step.end != null) { - lastSuccessfulStepName = step.name; - } - break; - case "termination": - break; - } - } - - return lastSuccessfulStepName; -}; diff --git a/packages/wrangler/src/workflows/commands/instances/index.ts b/packages/wrangler/src/workflows/commands/instances/index.ts deleted file mode 100644 index 51e29a8c03db..000000000000 --- a/packages/wrangler/src/workflows/commands/instances/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { instancesDescribeHandler, instancesDescribeOptions } from "./describe"; -import { instancesListHandler, instancesListOptions } from "./list"; -import { - instancesTerminateHandler, - instancesTerminateOptions, -} from "./terminate"; -import type { CommonYargsArgv } from "../../../yargs-types"; - -export const instances = (args: CommonYargsArgv) => { - return args - .command( - "list ", - "List workflow instances", - instancesListOptions, - instancesListHandler - ) - .command( - "describe ", - "Describe a workflow instance - see its logs, retries and errors", - instancesDescribeOptions, - instancesDescribeHandler - ) - .command( - "terminate ", - "Terminate a workflow instance", - instancesTerminateOptions, - instancesTerminateHandler - ); -}; diff --git a/packages/wrangler/src/workflows/commands/instances/list.ts b/packages/wrangler/src/workflows/commands/instances/list.ts deleted file mode 100644 index 652402a75def..000000000000 --- a/packages/wrangler/src/workflows/commands/instances/list.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { printWranglerBanner } from "../../.."; -import { fetchResult } from "../../../cfetch"; -import { readConfig } from "../../../config"; -import { logger } from "../../../logger"; -import { requireAuth } from "../../../user"; -import { emojifyInstanceStatus, validateStatus } from "../../utils"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../../yargs-types"; -import type { Instance } from "../../types"; - -export const instancesListOptions = (args: CommonYargsArgv) => { - return args - .positional("name", { - describe: "Name of the workflow", - type: "string", - demandOption: true, - }) - .option("reverse", { - describe: "Reverse order of the instances table", - type: "boolean", - default: false, - }) - .option("status", { - describe: - "Filters list by instance status (can be one of: queued, running, paused, errored, terminated, complete)", - type: "string", - }) - .option("page", { - describe: - 'Show a sepecific page from the listing, can configure page size using "per-page".', - type: "number", - default: 1, - }) - .option("per-page", { - describe: "Configure the maximum number of instances to show per page.", - type: "number", - }); -}; - -type HandlerOptions = StrictYargsOptionsToInterface< - typeof instancesListOptions ->; -export const instancesListHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); - - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); - - const URLParams = new URLSearchParams(); - - if (args.status !== undefined) { - const validatedStatus = validateStatus(args.status); - URLParams.set("status", validatedStatus); - } - if (args.perPage !== undefined) { - URLParams.set("per_page", args.perPage.toString()); - } - - URLParams.set("page", args.page.toString()); - - const instances = await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/instances`, - undefined, - URLParams - ); - - if (instances.length === 0) { - logger.warn( - `There are no instances in workflow "${args.name}". You can trigger it with "wrangler workflows trigger ${args.name}"` - ); - return; - } - - logger.info( - `Showing ${instances.length} instance${instances.length > 1 ? "s" : ""} from page ${args.page}:` - ); - - const prettierInstances = instances - .sort((a, b) => - args.reverse - ? a.modified_on.localeCompare(b.modified_on) - : b.modified_on.localeCompare(a.modified_on) - ) - .map((instance) => ({ - Id: instance.id, - Version: instance.version_id, - Created: new Date(instance.created_on).toLocaleString(), - Modified: new Date(instance.modified_on).toLocaleString(), - Status: emojifyInstanceStatus(instance.status), - })); - - logger.table(prettierInstances); -}; diff --git a/packages/wrangler/src/workflows/commands/instances/terminate.ts b/packages/wrangler/src/workflows/commands/instances/terminate.ts deleted file mode 100644 index 543d182b5780..000000000000 --- a/packages/wrangler/src/workflows/commands/instances/terminate.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { fetchResult } from "../../../cfetch"; -import { readConfig } from "../../../config"; -import { logger } from "../../../logger"; -import { printWranglerBanner } from "../../../update-check"; -import { requireAuth } from "../../../user"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../../yargs-types"; -import type { Instance } from "../../types"; - -export const instancesTerminateOptions = (args: CommonYargsArgv) => { - return args - .positional("name", { - describe: "Name of the workflow", - type: "string", - demandOption: true, - }) - .positional("id", { - describe: - "ID of the instance - instead of an UUID you can type 'latest' to get the latest instance and describe it", - type: "string", - demandOption: true, - }); -}; - -type HandlerOptions = StrictYargsOptionsToInterface< - typeof instancesTerminateOptions ->; - -export const instancesTerminateHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); - - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); - - let id = args.id; - - if (id == "latest") { - const instances = ( - await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/instances` - ) - ).sort((a, b) => b.created_on.localeCompare(a.created_on)); - - if (instances.length == 0) { - logger.error( - `There are no deployed instances in workflow "${args.name}".` - ); - return; - } - - id = instances[0].id; - } - - await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/instances/${id}`, - { - method: "DELETE", - } - ); - - logger.info( - `🥷 The instance "${id}" from ${args.name} was terminated successfully.` - ); -}; diff --git a/packages/wrangler/src/workflows/commands/list.ts b/packages/wrangler/src/workflows/commands/list.ts deleted file mode 100644 index 13d3ca1961fe..000000000000 --- a/packages/wrangler/src/workflows/commands/list.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { printWranglerBanner } from "../.."; -import { fetchResult } from "../../cfetch"; -import { readConfig } from "../../config"; -import { logger } from "../../logger"; -import { requireAuth } from "../../user"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; -import type { Workflow } from "../types"; - -export const workflowListOptions = (args: CommonYargsArgv) => { - return args - .option("page", { - describe: - 'Show a sepecific page from the listing, can configure page size using "per-page".', - type: "number", - default: 1, - }) - .option("per-page", { - describe: "Configure the maximum number of workflows to show per page.", - type: "number", - }); -}; - -type HandlerOptions = StrictYargsOptionsToInterface; -export const workflowListHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); - - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); - - const URLParams = new URLSearchParams(); - - if (args.perPage !== undefined) { - URLParams.set("per_page", args.perPage.toString()); - } - - URLParams.set("page", args.page.toString()); - - const workflows = await fetchResult( - `/accounts/${accountId}/workflows`, - undefined, - URLParams - ); - - if (workflows.length === 0) { - logger.warn("There are no deployed Workflows in this account."); - } else { - // TODO(lduarte): can we improve this message once pagination is deployed - logger.info( - `Showing last ${workflows.length} workflow${workflows.length > 1 ? "s" : ""}:` - ); - // sort by name and make the table head prettier by changing the keys - const prettierWorkflows = workflows - .sort((a, b) => a.name.localeCompare(b.name)) - .map((workflow) => ({ - Name: workflow.name, - "Script name": workflow.script_name, - "Class name": workflow.class_name, - Created: new Date(workflow.created_on).toLocaleString(), - Modified: new Date(workflow.modified_on).toLocaleString(), - })); - logger.table(prettierWorkflows); - } -}; diff --git a/packages/wrangler/src/workflows/commands/trigger.ts b/packages/wrangler/src/workflows/commands/trigger.ts deleted file mode 100644 index aa087a9fc80f..000000000000 --- a/packages/wrangler/src/workflows/commands/trigger.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { fetchResult } from "../../cfetch"; -import { readConfig } from "../../config"; -import { logger } from "../../logger"; -import { printWranglerBanner } from "../../update-check"; -import { requireAuth } from "../../user"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; -import type { InstanceWithoutDates } from "../types"; - -export const workflowTriggerOptions = (args: CommonYargsArgv) => { - return args - .positional("name", { - describe: "Name of the workflow", - type: "string", - demandOption: true, - }) - .positional("params", { - describe: "Params for the workflow instance, encoded as a JSON string", - type: "string", - default: "", - }); -}; - -type HandlerOptions = StrictYargsOptionsToInterface< - typeof workflowTriggerOptions ->; -export const workflowTriggerHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); - - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); - - if (args.params.length != 0) { - try { - JSON.parse(args.params); - } catch (e) { - logger.error( - `Error while parsing instance parameters: "${args.params}" with ${e}' ` - ); - return; - } - } - - const response = await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/instances`, - { - method: "POST", - body: args.params.length != 0 ? args.params : undefined, - } - ); - - logger.info( - `🚀 Workflow instance "${response.id}" has been queued successfully` - ); -}; diff --git a/packages/wrangler/src/workflows/utils.ts b/packages/wrangler/src/workflows/utils.ts deleted file mode 100644 index f4d119041af3..000000000000 --- a/packages/wrangler/src/workflows/utils.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { UserError } from "../errors"; -import type { InstanceStatus, InstanceTriggerName } from "./types"; - -export const emojifyInstanceStatus = (status: InstanceStatus) => { - switch (status) { - case "complete": - return "✅ Completed"; - case "errored": - return "❌ Errored"; - case "unknown": - return "❓ Unknown"; - case "paused": - return "⏸️ Paused"; - case "queued": - return "⌛ Queued"; - case "running": - return "▶ Running"; - case "terminated": - return "🚫 Terminated"; - } -}; - -export const emojifyInstanceTriggerName = (status: InstanceTriggerName) => { - switch (status) { - case "api": - return "🌎 API"; - case "binding": - return "🔗 Binding"; - case "cron": - return "⌛ Cron"; - case "event": - return "📩 Event"; - default: - return "❓ Unknown"; - } -}; - -export const emojifyStepType = (type: string) => { - switch (type) { - case "step": - return "🎯 Step"; - case "sleep": - return "💤 Sleeping"; - case "termination": - return "🚫 Termination"; - default: - return "❓ Unknown"; - } -}; - -export const validateStatus = (status: string): InstanceStatus => { - switch (status) { - case "complete": - return "complete"; - case "errored": - return "errored"; - case "paused": - return "paused"; - case "queued": - return "queued"; - case "running": - return "running"; - case "terminated": - return "terminated"; - default: - throw new UserError( - `Looks like you have provided a invalid status "${status}". Valid statuses are: queued, running, paused, errored, terminated, complete` - ); - } -}; diff --git a/packages/wrangler/src/workflows/workflows.ts b/packages/wrangler/src/workflows/workflows.ts deleted file mode 100644 index 5d086a8b42ed..000000000000 --- a/packages/wrangler/src/workflows/workflows.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { type CommonYargsArgv, type SubHelp } from "../yargs-types"; -import { - workflowDeleteHandler, - workflowDeleteOptions, -} from "./commands/delete"; -import { - workflowDescribeHandler, - workflowDescribeOptions, -} from "./commands/describe"; -import { instances } from "./commands/instances"; -import { workflowListHandler, workflowListOptions } from "./commands/list"; -import { - workflowTriggerHandler, - workflowTriggerOptions, -} from "./commands/trigger"; - -const workflowsEpilog = - "🚨 'wrangler workflows ...' commands are currently in private beta. If your account isn't authorized, your commands will fail."; - -export const workflows = (yargs: CommonYargsArgv, subHelp: SubHelp) => { - return yargs - .command( - "list", - "List Workflows associated to account", - workflowListOptions, - workflowListHandler - ) - .command( - "describe ", - "Describe Workflow resource", - workflowDescribeOptions, - workflowDescribeHandler - ) - .command( - "delete ", - "Delete workflow - when deleting a workflow, it will also delete it's own instances", - workflowDeleteOptions, - workflowDeleteHandler - ) - .command( - "trigger [params]", - "Trigger a workflow, creating a new instance. Can optionally take a JSON string to pass a parameter into the workflow instance.", - workflowTriggerOptions, - workflowTriggerHandler - ) - .command( - "instances", - "Instance related commands (list, describe, terminate...)", - (instancesYargs) => { - return instances(instancesYargs).command(subHelp); - } - ) - .epilog(workflowsEpilog); -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20cf9d62f28f..62b6a29b915c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1647,15 +1647,9 @@ importers: chokidar: specifier: ^3.5.3 version: 3.5.3 - date-fns: - specifier: ^4.1.0 - version: 4.1.0 esbuild: specifier: 0.17.19 version: 0.17.19 - itty-time: - specifier: ^1.0.6 - version: 1.0.6 miniflare: specifier: workspace:* version: link:../miniflare @@ -4599,9 +4593,6 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} - date-fns@4.1.0: - resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} - date-time@3.1.0: resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} engines: {node: '>=6'} @@ -5927,9 +5918,6 @@ packages: itty-router@4.0.17: resolution: {integrity: sha512-Fu/GO3MFX6Hwd+QF1/BFjoFpPQKh2Bu4DtBdse5kECcUwldNBrZqgwq0IariLrP67iADGlzkHIlOWwJ8F4SJ4A==} - itty-time@1.0.6: - resolution: {integrity: sha512-+P8IZaLLBtFv8hCkIjcymZOp4UJ+xW6bSlQsXGqrkmJh7vSiMFSlNne0mCYagEE0N7HDNR5jJBRxwN0oYv61Rw==} - jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} @@ -11560,8 +11548,6 @@ snapshots: dependencies: '@babel/runtime': 7.22.5 - date-fns@4.1.0: {} - date-time@3.1.0: dependencies: time-zone: 1.0.0 @@ -13127,8 +13113,6 @@ snapshots: itty-router@4.0.17: {} - itty-time@1.0.6: {} - jackspeak@2.3.6: dependencies: '@isaacs/cliui': 8.0.2 From 5a2d0efbe2357ac1b197eeee8576687e92802fd3 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:00:44 +0100 Subject: [PATCH 03/16] add Workflow support to `wrangler types` --- fixtures/workflow/worker-configuration.d.ts | 5 +++++ packages/wrangler/src/type-generation/index.ts | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 fixtures/workflow/worker-configuration.d.ts diff --git a/fixtures/workflow/worker-configuration.d.ts b/fixtures/workflow/worker-configuration.d.ts new file mode 100644 index 000000000000..ad79d683e0fb --- /dev/null +++ b/fixtures/workflow/worker-configuration.d.ts @@ -0,0 +1,5 @@ +// Generated by Wrangler by running `wrangler types` + +interface Env { + WORKFLOW: Workflow; +} diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index f54c2e72fa72..10e0e78d11d9 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -138,6 +138,7 @@ export async function typesHandler( version_metadata: config.version_metadata, secrets, assets: config.assets, + workflows: config.workflows, }; await generateTypes( @@ -427,6 +428,12 @@ async function generateTypes( envTypeStructure.push(constructType(configToDTS.assets.binding, "Fetcher")); } + if (configToDTS.workflows) { + for (const workflow of configToDTS.workflows) { + envTypeStructure.push(constructType(workflow.binding, "Workflow")); + } + } + const modulesTypeStructure: string[] = []; if (configToDTS.rules) { const moduleTypeMap = { From 0f6ca2eacf23511aa2327758ad9295deef8cd3f6 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:30:54 +0100 Subject: [PATCH 04/16] update fixture --- fixtures/workflow/package.json | 2 +- fixtures/workflow/wrangler.toml | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/fixtures/workflow/package.json b/fixtures/workflow/package.json index 9fc4ce659562..8c9f479ccee9 100644 --- a/fixtures/workflow/package.json +++ b/fixtures/workflow/package.json @@ -6,7 +6,7 @@ "start": "wrangler dev --x-dev-env" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20240909.0", + "@cloudflare/workers-types": "^4.20241011.0", "wrangler": "workspace:*" }, "volta": { diff --git a/fixtures/workflow/wrangler.toml b/fixtures/workflow/wrangler.toml index ee5fe0862416..6cf6d4593655 100644 --- a/fixtures/workflow/wrangler.toml +++ b/fixtures/workflow/wrangler.toml @@ -1,10 +1,9 @@ #:schema node_modules/wrangler/config-schema.json name = "my-workflow-demo" main = "src/index.ts" -compatibility_date = "2024-09-02" -compatibility_flags = ["experimental"] +compatibility_date = "2024-10-11" [[workflows]] binding = "WORKFLOW" name = "my-workflow" -class_name = "Demo" \ No newline at end of file +class_name = "Demo" From 0d9880b004340253b64edbccf029bdf091ccd4ca Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:31:40 +0100 Subject: [PATCH 05/16] add (skipped) e2e test --- .../wrangler/e2e/dev-with-resources.test.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/wrangler/e2e/dev-with-resources.test.ts b/packages/wrangler/e2e/dev-with-resources.test.ts index 78fcfab31b88..2168706ad6a1 100644 --- a/packages/wrangler/e2e/dev-with-resources.test.ts +++ b/packages/wrangler/e2e/dev-with-resources.test.ts @@ -528,6 +528,47 @@ describe.sequential.each(RUNTIMES)("Bindings: $flags", ({ runtime, flags }) => { await worker.readUntil(/✉️/); }); + // TODO: enable for remove dev once realish preview supports it + // TODO: enable for local dev once implemented + it.skip("exposes Workflow bindings", async () => { + await helper.seed({ + "wrangler.toml": dedent` + name = "my-workflow-demo" + main = "src/index.ts" + compatibility_date = "2024-10-11" + + [[workflows]] + binding = "WORKFLOW" + name = "my-workflow" + class_name = "Demo" + `, + "src/index.ts": dedent` + import { WorkflowEntrypoint } from "cloudflare:workers"; + + export default { + async fetch(request, env, ctx) { + if (env.WORKFLOW === undefined) { + return new Response("env.WORKFLOW is undefined"); + } + + return new Response("env.WORKFLOW is available"); + } + } + + export class Demo extends WorkflowEntrypoint { + run() { + // blank + } + } + `, + }); + const worker = helper.runLongLived(`wrangler dev ${flags}`); + const { url } = await worker.waitForReady(); + const res = await fetch(url); + + await expect(res.text()).resolves.toBe("env.WORKFLOW is available"); + }); + // TODO(soon): implement E2E tests for other bindings it.todo("exposes hyperdrive bindings"); it.skipIf(isLocal).todo("exposes send email bindings"); From b8d41b26cc1a523a5cebf934c3391bec1cd1db86 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Thu, 17 Oct 2024 23:10:25 +0100 Subject: [PATCH 06/16] fix pnpm-lock.yaml --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62b6a29b915c..4e61acb2d84c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -741,7 +741,7 @@ importers: fixtures/workflow: devDependencies: '@cloudflare/workers-types': - specifier: ^4.20240909.0 + specifier: ^4.20241011.0 version: 4.20241011.0 wrangler: specifier: workspace:* From 37ea25285d952c93122d69475c0f5deb5f950eef Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Thu, 17 Oct 2024 23:29:05 +0100 Subject: [PATCH 07/16] fix test --- packages/wrangler/src/__tests__/configuration.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wrangler/src/__tests__/configuration.test.ts b/packages/wrangler/src/__tests__/configuration.test.ts index 07709626448f..7490024f4761 100644 --- a/packages/wrangler/src/__tests__/configuration.test.ts +++ b/packages/wrangler/src/__tests__/configuration.test.ts @@ -131,6 +131,7 @@ describe("normalizeAndValidateConfig()", () => { placement: undefined, tail_consumers: undefined, pipelines: [], + workflows: [], }); expect(diagnostics.hasErrors()).toBe(false); expect(diagnostics.hasWarnings()).toBe(false); From 9f6adc908aee06209b3720feff602fe8e87faa77 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Thu, 17 Oct 2024 23:54:38 +0100 Subject: [PATCH 08/16] add changeset --- .changeset/green-parrots-lick.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .changeset/green-parrots-lick.md diff --git a/.changeset/green-parrots-lick.md b/.changeset/green-parrots-lick.md new file mode 100644 index 000000000000..b0fe55f3627d --- /dev/null +++ b/.changeset/green-parrots-lick.md @@ -0,0 +1,22 @@ +--- +"wrangler": minor +--- + +Add support for Workflow bindings (in deployments, not yet in local dev) + +To bind to a workflow, add a `workflows` section in your wrangler.toml: + +```toml +[[workflows]] +binding = "WORKFLOW" +name = "my-workflow" +class_name = "MyDemoWorkflow" +``` + +and export an entrypoint (e.g. `MyDemoWorkflow`) in your script: + +```typescript +import { WorkflowEntrypoint } from "cloudflare:workers"; + +export class MyDemoWorkflow extends WorkflowEntrypoint<{}, Params> {...} +``` From 80af061ac87a9c7d44fb0707ff41e629c10cd1b9 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Fri, 18 Oct 2024 01:06:19 +0100 Subject: [PATCH 09/16] add commands --- packages/wrangler/src/index.ts | 6 + .../wrangler/src/workflows/commands/delete.ts | 33 +++ .../src/workflows/commands/describe.ts | 62 ++++ .../workflows/commands/instances/describe.ts | 280 ++++++++++++++++++ .../src/workflows/commands/instances/index.ts | 29 ++ .../src/workflows/commands/instances/list.ts | 95 ++++++ .../workflows/commands/instances/terminate.ts | 66 +++++ .../wrangler/src/workflows/commands/list.ts | 66 +++++ .../src/workflows/commands/trigger.ts | 57 ++++ packages/wrangler/src/workflows/utils.ts | 70 +++++ packages/wrangler/src/workflows/workflows.ts | 54 ++++ 11 files changed, 818 insertions(+) create mode 100644 packages/wrangler/src/workflows/commands/delete.ts create mode 100644 packages/wrangler/src/workflows/commands/describe.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/describe.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/index.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/list.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/terminate.ts create mode 100644 packages/wrangler/src/workflows/commands/list.ts create mode 100644 packages/wrangler/src/workflows/commands/trigger.ts create mode 100644 packages/wrangler/src/workflows/utils.ts create mode 100644 packages/wrangler/src/workflows/workflows.ts diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index b6b1422beed3..7d74352f4895 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -77,6 +77,7 @@ import registerVersionsSubcommands from "./versions"; import registerVersionsDeploymentsSubcommands from "./versions/deployments"; import registerVersionsRollbackCommand from "./versions/rollback"; import { whoami } from "./whoami"; +import { workflows } from "./workflows/workflows"; import { asJson } from "./yargs-types"; import type { Config } from "./config"; import type { LoggerLevel } from "./logger"; @@ -613,6 +614,11 @@ export function createCLIParser(argv: string[]) { return pipelines(pipelinesYargs.command(subHelp)); }); + // workflows + wrangler.command("workflows", false, (workflowArgs) => { + return workflows(workflowArgs.command(subHelp), subHelp); + }); + /******************** CMD GROUP ***********************/ // login wrangler.command( diff --git a/packages/wrangler/src/workflows/commands/delete.ts b/packages/wrangler/src/workflows/commands/delete.ts new file mode 100644 index 000000000000..53cf826f14f7 --- /dev/null +++ b/packages/wrangler/src/workflows/commands/delete.ts @@ -0,0 +1,33 @@ +import { fetchResult } from "../../cfetch"; +import { readConfig } from "../../config"; +import { logger } from "../../logger"; +import { printWranglerBanner } from "../../update-check"; +import { requireAuth } from "../../user"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../yargs-types"; + +export const workflowDeleteOptions = (args: CommonYargsArgv) => { + return args.positional("name", { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface< + typeof workflowDeleteOptions +>; +export const workflowDeleteHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + await fetchResult(`/accounts/${accountId}/workflows/${args.name}`, { + method: "DELETE", + }); + + logger.info(`Workflow "${args.name}" was successfully removed`); +}; diff --git a/packages/wrangler/src/workflows/commands/describe.ts b/packages/wrangler/src/workflows/commands/describe.ts new file mode 100644 index 000000000000..36b7ef6d62d4 --- /dev/null +++ b/packages/wrangler/src/workflows/commands/describe.ts @@ -0,0 +1,62 @@ +import { logRaw } from "@cloudflare/cli"; +import { white } from "@cloudflare/cli/colors"; +import { fetchResult } from "../../cfetch"; +import { readConfig } from "../../config"; +import { printWranglerBanner } from "../../update-check"; +import { requireAuth } from "../../user"; +import formatLabelledValues from "../../utils/render-labelled-values"; +import { + type CommonYargsArgv, + type StrictYargsOptionsToInterface, +} from "../../yargs-types"; +import type { Version, Workflow } from "../types"; + +export const workflowDescribeOptions = (args: CommonYargsArgv) => { + return args.positional("name", { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface< + typeof workflowDescribeOptions +>; +export const workflowDescribeHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + const workflow = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}` + ); + + const versions = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/versions` + ); + + const latestVersion = versions[0]; + + logRaw( + formatLabelledValues({ + Name: workflow.name, + Id: workflow.id, + "Script Name": workflow.script_name, + "Class Name": workflow.class_name, + "Created On": workflow.created_on, + "Modified On": workflow.modified_on, + }) + ); + logRaw(white("Latest Version:")); + logRaw( + formatLabelledValues( + { + Id: latestVersion.id, + "Created On": workflow.created_on, + "Modified On": workflow.modified_on, + }, + { indentationCount: 2 } + ) + ); +}; diff --git a/packages/wrangler/src/workflows/commands/instances/describe.ts b/packages/wrangler/src/workflows/commands/instances/describe.ts new file mode 100644 index 000000000000..f227e941d9fe --- /dev/null +++ b/packages/wrangler/src/workflows/commands/instances/describe.ts @@ -0,0 +1,280 @@ +import { logRaw } from "@cloudflare/cli"; +import { red, white } from "@cloudflare/cli/colors"; +import { + addMilliseconds, + formatDistanceStrict, + formatDistanceToNowStrict, +} from "date-fns"; +import { ms } from "itty-time"; +import { fetchResult } from "../../../cfetch"; +import { readConfig } from "../../../config"; +import { logger } from "../../../logger"; +import { printWranglerBanner } from "../../../update-check"; +import { requireAuth } from "../../../user"; +import formatLabelledValues from "../../../utils/render-labelled-values"; +import { + emojifyInstanceStatus, + emojifyInstanceTriggerName, + emojifyStepType, +} from "../../utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../../yargs-types"; +import type { + Instance, + InstanceSleepLog, + InstanceStatusAndLogs, + InstanceStepLog, + InstanceTerminateLog, +} from "../../types"; + +export const instancesDescribeOptions = (args: CommonYargsArgv) => { + return args + .positional("name", { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }) + .positional("id", { + describe: + "ID of the instance - instead of an UUID you can type 'latest' to get the latest instance and describe it", + type: "string", + demandOption: true, + }) + .option("step-output", { + describe: + "Don't output the step output since it might clutter the terminal", + type: "boolean", + default: true, + }) + .option("truncate-output-limit", { + describe: "Truncate step output after x characters", + type: "number", + default: 5000, + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface< + typeof instancesDescribeOptions +>; + +export const instancesDescribeHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + let id = args.id; + + if (id == "latest") { + const instances = ( + await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances` + ) + ).sort((a, b) => b.created_on.localeCompare(a.created_on)); + + if (instances.length == 0) { + logger.error( + `There are no deployed instances in workflow "${args.name}".` + ); + return; + } + + id = instances[0].id; + } + + const instance = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances/${id}` + ); + + const formattedInstance: Record = { + "Workflow Name": args.name, + "Instance Id": id, + "Version Id": instance.versionId, + Status: emojifyInstanceStatus(instance.status), + Trigger: emojifyInstanceTriggerName(instance.trigger.source), + Queued: new Date(instance.queued).toLocaleString(), + }; + + if (instance.success != null) { + formattedInstance.Success = instance.success ? "✅ Yes" : "❌ No"; + } + + // date related stuff, if the workflow is still running assume duration until now + if (instance.start != undefined) { + formattedInstance.Start = new Date(instance.start).toLocaleString(); + } + + if (instance.end != undefined) { + formattedInstance.End = new Date(instance.end).toLocaleString(); + } + + if (instance.start != null && instance.end != null) { + formattedInstance.Duration = formatDistanceStrict( + new Date(instance.end), + new Date(instance.start) + ); + } else if (instance.start != null) { + // Convert current date to UTC + formattedInstance.Duration = formatDistanceStrict( + new Date(instance.start), + new Date(new Date().toUTCString().slice(0, -4)) + ); + } + + const lastSuccessfulStepName = getLastSuccessfulStep(instance); + if (lastSuccessfulStepName != null) { + formattedInstance["Last Successful Step"] = lastSuccessfulStepName; + } + + // display the error if the instance errored out + if (instance.error != null) { + formattedInstance.Error = red( + `${instance.error.name}: ${instance.error.message}` + ); + } + + logRaw(formatLabelledValues(formattedInstance)); + logRaw(white("Steps:")); + + instance.steps.forEach(logStep.bind(false, args)); +}; + +const logStep = ( + args: HandlerOptions, + step: InstanceStepLog | InstanceSleepLog | InstanceTerminateLog +) => { + logRaw(""); + const formattedStep: Record = {}; + + if (step.type == "sleep" || step.type == "step") { + formattedStep.Name = step.name; + formattedStep.Type = emojifyStepType(step.type); + + // date related stuff, if the step is still running assume duration until now + if (step.start != undefined) { + formattedStep.Start = new Date(step.start).toLocaleString(); + } + + if (step.end != undefined) { + formattedStep.End = new Date(step.end).toLocaleString(); + } + + if (step.start != null && step.end != null) { + formattedStep.Duration = formatDistanceStrict( + new Date(step.end), + new Date(step.start) + ); + } else if (step.start != null) { + // Convert current date to UTC + formattedStep.Duration = formatDistanceStrict( + new Date(step.start), + new Date(new Date().toUTCString().slice(0, -4)) + ); + } + } else if (step.type == "termination") { + formattedStep.Type = emojifyStepType(step.type); + formattedStep.Trigger = step.trigger.source; + } + + if (step.type == "step") { + if (step.success !== null) { + formattedStep.Success = step.success ? "✅ Yes" : "❌ No"; + } else { + formattedStep.Success = "▶ Running"; + } + + if (step.success === null) { + const latestAttempt = step.attempts.at(-1); + let delay = step.config.retries.delay; + if (latestAttempt !== undefined && latestAttempt.success === false) { + // SAFETY: It's okay because end date must always exist in the API, otherwise it's okay to fail + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const endDate = new Date(latestAttempt.end!); + if (typeof delay === "string") { + delay = ms(delay); + } + const retryDate = addMilliseconds(endDate, delay); + formattedStep["Retries At"] = + `${retryDate.toLocaleString()} (in ${formatDistanceToNowStrict(retryDate)} from now)`; + } + } + if (step.output !== undefined && args.stepOutput) { + let output: string; + try { + output = JSON.stringify(step.output); + } catch { + output = step.output as string; + } + formattedStep.Output = + output.length > args.truncateOutputLimit + ? output.substring(0, args.truncateOutputLimit) + + "[...output truncated]" + : output; + } + } + + logRaw(formatLabelledValues(formattedStep, { indentationCount: 2 })); + + if (step.type == "step") { + const prettyAttempts = step.attempts.map((val) => { + const attempt: Record = {}; + + attempt.Start = new Date(val.start).toLocaleString(); + attempt.End = val.end == null ? "" : new Date(val.end).toLocaleString(); + + if (val.start != null && val.end != null) { + attempt.Duration = formatDistanceStrict( + new Date(val.end), + new Date(val.start) + ); + } else if (val.start != null) { + // Converting datetimes into UTC is very cool in JS + attempt.Duration = formatDistanceStrict( + new Date(val.start), + new Date(new Date().toUTCString().slice(0, -4)) + ); + } + + attempt.State = + val.success == null + ? "🔄 Working" + : val.success + ? "✅ Success" + : "❌ Error"; + + // This is actually safe to do while logger.table only considers the first element as keys. + // Because if there's an error, the first row will always be an error + if (val.error != null) { + attempt.Error = red(`${val.error.name}: ${val.error.message}`); + } + return attempt; + }); + + logger.table(prettyAttempts); + } +}; + +const getLastSuccessfulStep = (logs: InstanceStatusAndLogs): string | null => { + let lastSuccessfulStepName: string | null = null; + + for (const step of logs.steps) { + switch (step.type) { + case "step": + if (step.success == true) { + lastSuccessfulStepName = step.name; + } + break; + case "sleep": + if (step.end != null) { + lastSuccessfulStepName = step.name; + } + break; + case "termination": + break; + } + } + + return lastSuccessfulStepName; +}; diff --git a/packages/wrangler/src/workflows/commands/instances/index.ts b/packages/wrangler/src/workflows/commands/instances/index.ts new file mode 100644 index 000000000000..51e29a8c03db --- /dev/null +++ b/packages/wrangler/src/workflows/commands/instances/index.ts @@ -0,0 +1,29 @@ +import { instancesDescribeHandler, instancesDescribeOptions } from "./describe"; +import { instancesListHandler, instancesListOptions } from "./list"; +import { + instancesTerminateHandler, + instancesTerminateOptions, +} from "./terminate"; +import type { CommonYargsArgv } from "../../../yargs-types"; + +export const instances = (args: CommonYargsArgv) => { + return args + .command( + "list ", + "List workflow instances", + instancesListOptions, + instancesListHandler + ) + .command( + "describe ", + "Describe a workflow instance - see its logs, retries and errors", + instancesDescribeOptions, + instancesDescribeHandler + ) + .command( + "terminate ", + "Terminate a workflow instance", + instancesTerminateOptions, + instancesTerminateHandler + ); +}; diff --git a/packages/wrangler/src/workflows/commands/instances/list.ts b/packages/wrangler/src/workflows/commands/instances/list.ts new file mode 100644 index 000000000000..652402a75def --- /dev/null +++ b/packages/wrangler/src/workflows/commands/instances/list.ts @@ -0,0 +1,95 @@ +import { printWranglerBanner } from "../../.."; +import { fetchResult } from "../../../cfetch"; +import { readConfig } from "../../../config"; +import { logger } from "../../../logger"; +import { requireAuth } from "../../../user"; +import { emojifyInstanceStatus, validateStatus } from "../../utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../../yargs-types"; +import type { Instance } from "../../types"; + +export const instancesListOptions = (args: CommonYargsArgv) => { + return args + .positional("name", { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }) + .option("reverse", { + describe: "Reverse order of the instances table", + type: "boolean", + default: false, + }) + .option("status", { + describe: + "Filters list by instance status (can be one of: queued, running, paused, errored, terminated, complete)", + type: "string", + }) + .option("page", { + describe: + 'Show a sepecific page from the listing, can configure page size using "per-page".', + type: "number", + default: 1, + }) + .option("per-page", { + describe: "Configure the maximum number of instances to show per page.", + type: "number", + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface< + typeof instancesListOptions +>; +export const instancesListHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + const URLParams = new URLSearchParams(); + + if (args.status !== undefined) { + const validatedStatus = validateStatus(args.status); + URLParams.set("status", validatedStatus); + } + if (args.perPage !== undefined) { + URLParams.set("per_page", args.perPage.toString()); + } + + URLParams.set("page", args.page.toString()); + + const instances = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances`, + undefined, + URLParams + ); + + if (instances.length === 0) { + logger.warn( + `There are no instances in workflow "${args.name}". You can trigger it with "wrangler workflows trigger ${args.name}"` + ); + return; + } + + logger.info( + `Showing ${instances.length} instance${instances.length > 1 ? "s" : ""} from page ${args.page}:` + ); + + const prettierInstances = instances + .sort((a, b) => + args.reverse + ? a.modified_on.localeCompare(b.modified_on) + : b.modified_on.localeCompare(a.modified_on) + ) + .map((instance) => ({ + Id: instance.id, + Version: instance.version_id, + Created: new Date(instance.created_on).toLocaleString(), + Modified: new Date(instance.modified_on).toLocaleString(), + Status: emojifyInstanceStatus(instance.status), + })); + + logger.table(prettierInstances); +}; diff --git a/packages/wrangler/src/workflows/commands/instances/terminate.ts b/packages/wrangler/src/workflows/commands/instances/terminate.ts new file mode 100644 index 000000000000..543d182b5780 --- /dev/null +++ b/packages/wrangler/src/workflows/commands/instances/terminate.ts @@ -0,0 +1,66 @@ +import { fetchResult } from "../../../cfetch"; +import { readConfig } from "../../../config"; +import { logger } from "../../../logger"; +import { printWranglerBanner } from "../../../update-check"; +import { requireAuth } from "../../../user"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../../yargs-types"; +import type { Instance } from "../../types"; + +export const instancesTerminateOptions = (args: CommonYargsArgv) => { + return args + .positional("name", { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }) + .positional("id", { + describe: + "ID of the instance - instead of an UUID you can type 'latest' to get the latest instance and describe it", + type: "string", + demandOption: true, + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface< + typeof instancesTerminateOptions +>; + +export const instancesTerminateHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + let id = args.id; + + if (id == "latest") { + const instances = ( + await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances` + ) + ).sort((a, b) => b.created_on.localeCompare(a.created_on)); + + if (instances.length == 0) { + logger.error( + `There are no deployed instances in workflow "${args.name}".` + ); + return; + } + + id = instances[0].id; + } + + await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances/${id}`, + { + method: "DELETE", + } + ); + + logger.info( + `🥷 The instance "${id}" from ${args.name} was terminated successfully.` + ); +}; diff --git a/packages/wrangler/src/workflows/commands/list.ts b/packages/wrangler/src/workflows/commands/list.ts new file mode 100644 index 000000000000..13d3ca1961fe --- /dev/null +++ b/packages/wrangler/src/workflows/commands/list.ts @@ -0,0 +1,66 @@ +import { printWranglerBanner } from "../.."; +import { fetchResult } from "../../cfetch"; +import { readConfig } from "../../config"; +import { logger } from "../../logger"; +import { requireAuth } from "../../user"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../yargs-types"; +import type { Workflow } from "../types"; + +export const workflowListOptions = (args: CommonYargsArgv) => { + return args + .option("page", { + describe: + 'Show a sepecific page from the listing, can configure page size using "per-page".', + type: "number", + default: 1, + }) + .option("per-page", { + describe: "Configure the maximum number of workflows to show per page.", + type: "number", + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface; +export const workflowListHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + const URLParams = new URLSearchParams(); + + if (args.perPage !== undefined) { + URLParams.set("per_page", args.perPage.toString()); + } + + URLParams.set("page", args.page.toString()); + + const workflows = await fetchResult( + `/accounts/${accountId}/workflows`, + undefined, + URLParams + ); + + if (workflows.length === 0) { + logger.warn("There are no deployed Workflows in this account."); + } else { + // TODO(lduarte): can we improve this message once pagination is deployed + logger.info( + `Showing last ${workflows.length} workflow${workflows.length > 1 ? "s" : ""}:` + ); + // sort by name and make the table head prettier by changing the keys + const prettierWorkflows = workflows + .sort((a, b) => a.name.localeCompare(b.name)) + .map((workflow) => ({ + Name: workflow.name, + "Script name": workflow.script_name, + "Class name": workflow.class_name, + Created: new Date(workflow.created_on).toLocaleString(), + Modified: new Date(workflow.modified_on).toLocaleString(), + })); + logger.table(prettierWorkflows); + } +}; diff --git a/packages/wrangler/src/workflows/commands/trigger.ts b/packages/wrangler/src/workflows/commands/trigger.ts new file mode 100644 index 000000000000..aa087a9fc80f --- /dev/null +++ b/packages/wrangler/src/workflows/commands/trigger.ts @@ -0,0 +1,57 @@ +import { fetchResult } from "../../cfetch"; +import { readConfig } from "../../config"; +import { logger } from "../../logger"; +import { printWranglerBanner } from "../../update-check"; +import { requireAuth } from "../../user"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../yargs-types"; +import type { InstanceWithoutDates } from "../types"; + +export const workflowTriggerOptions = (args: CommonYargsArgv) => { + return args + .positional("name", { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }) + .positional("params", { + describe: "Params for the workflow instance, encoded as a JSON string", + type: "string", + default: "", + }); +}; + +type HandlerOptions = StrictYargsOptionsToInterface< + typeof workflowTriggerOptions +>; +export const workflowTriggerHandler = async (args: HandlerOptions) => { + await printWranglerBanner(); + + const config = readConfig(args.config, args); + const accountId = await requireAuth(config); + + if (args.params.length != 0) { + try { + JSON.parse(args.params); + } catch (e) { + logger.error( + `Error while parsing instance parameters: "${args.params}" with ${e}' ` + ); + return; + } + } + + const response = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances`, + { + method: "POST", + body: args.params.length != 0 ? args.params : undefined, + } + ); + + logger.info( + `🚀 Workflow instance "${response.id}" has been queued successfully` + ); +}; diff --git a/packages/wrangler/src/workflows/utils.ts b/packages/wrangler/src/workflows/utils.ts new file mode 100644 index 000000000000..f4d119041af3 --- /dev/null +++ b/packages/wrangler/src/workflows/utils.ts @@ -0,0 +1,70 @@ +import { UserError } from "../errors"; +import type { InstanceStatus, InstanceTriggerName } from "./types"; + +export const emojifyInstanceStatus = (status: InstanceStatus) => { + switch (status) { + case "complete": + return "✅ Completed"; + case "errored": + return "❌ Errored"; + case "unknown": + return "❓ Unknown"; + case "paused": + return "⏸️ Paused"; + case "queued": + return "⌛ Queued"; + case "running": + return "▶ Running"; + case "terminated": + return "🚫 Terminated"; + } +}; + +export const emojifyInstanceTriggerName = (status: InstanceTriggerName) => { + switch (status) { + case "api": + return "🌎 API"; + case "binding": + return "🔗 Binding"; + case "cron": + return "⌛ Cron"; + case "event": + return "📩 Event"; + default: + return "❓ Unknown"; + } +}; + +export const emojifyStepType = (type: string) => { + switch (type) { + case "step": + return "🎯 Step"; + case "sleep": + return "💤 Sleeping"; + case "termination": + return "🚫 Termination"; + default: + return "❓ Unknown"; + } +}; + +export const validateStatus = (status: string): InstanceStatus => { + switch (status) { + case "complete": + return "complete"; + case "errored": + return "errored"; + case "paused": + return "paused"; + case "queued": + return "queued"; + case "running": + return "running"; + case "terminated": + return "terminated"; + default: + throw new UserError( + `Looks like you have provided a invalid status "${status}". Valid statuses are: queued, running, paused, errored, terminated, complete` + ); + } +}; diff --git a/packages/wrangler/src/workflows/workflows.ts b/packages/wrangler/src/workflows/workflows.ts new file mode 100644 index 000000000000..5d086a8b42ed --- /dev/null +++ b/packages/wrangler/src/workflows/workflows.ts @@ -0,0 +1,54 @@ +import { type CommonYargsArgv, type SubHelp } from "../yargs-types"; +import { + workflowDeleteHandler, + workflowDeleteOptions, +} from "./commands/delete"; +import { + workflowDescribeHandler, + workflowDescribeOptions, +} from "./commands/describe"; +import { instances } from "./commands/instances"; +import { workflowListHandler, workflowListOptions } from "./commands/list"; +import { + workflowTriggerHandler, + workflowTriggerOptions, +} from "./commands/trigger"; + +const workflowsEpilog = + "🚨 'wrangler workflows ...' commands are currently in private beta. If your account isn't authorized, your commands will fail."; + +export const workflows = (yargs: CommonYargsArgv, subHelp: SubHelp) => { + return yargs + .command( + "list", + "List Workflows associated to account", + workflowListOptions, + workflowListHandler + ) + .command( + "describe ", + "Describe Workflow resource", + workflowDescribeOptions, + workflowDescribeHandler + ) + .command( + "delete ", + "Delete workflow - when deleting a workflow, it will also delete it's own instances", + workflowDeleteOptions, + workflowDeleteHandler + ) + .command( + "trigger [params]", + "Trigger a workflow, creating a new instance. Can optionally take a JSON string to pass a parameter into the workflow instance.", + workflowTriggerOptions, + workflowTriggerHandler + ) + .command( + "instances", + "Instance related commands (list, describe, terminate...)", + (instancesYargs) => { + return instances(instancesYargs).command(subHelp); + } + ) + .epilog(workflowsEpilog); +}; From 2ceb959df7cbf5c55ff2e02f949bc68519808f06 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Fri, 18 Oct 2024 01:10:50 +0100 Subject: [PATCH 10/16] add command description --- packages/wrangler/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index 7d74352f4895..19d0bd6c3275 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -615,7 +615,7 @@ export function createCLIParser(argv: string[]) { }); // workflows - wrangler.command("workflows", false, (workflowArgs) => { + wrangler.command("workflows", "🧑‍🍳 Manage Workflows", (workflowArgs) => { return workflows(workflowArgs.command(subHelp), subHelp); }); From 1ad3b9a66180277d3e571b24d2a5f80549ae87f0 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Fri, 18 Oct 2024 01:13:30 +0100 Subject: [PATCH 11/16] change private beta message to open beta --- packages/wrangler/src/workflows/workflows.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/wrangler/src/workflows/workflows.ts b/packages/wrangler/src/workflows/workflows.ts index 5d086a8b42ed..a8e5310a7371 100644 --- a/packages/wrangler/src/workflows/workflows.ts +++ b/packages/wrangler/src/workflows/workflows.ts @@ -14,8 +14,7 @@ import { workflowTriggerOptions, } from "./commands/trigger"; -const workflowsEpilog = - "🚨 'wrangler workflows ...' commands are currently in private beta. If your account isn't authorized, your commands will fail."; +const workflowsEpilog = "🚨 Workflows is currently in Open Beta."; export const workflows = (yargs: CommonYargsArgv, subHelp: SubHelp) => { return yargs From 998453fed8856b867a7112fce9dd3d80bfb7e6e2 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Fri, 18 Oct 2024 01:51:14 +0100 Subject: [PATCH 12/16] refactor to use defineCommand util --- packages/wrangler/package.json | 2 + packages/wrangler/src/core/teams.d.ts | 1 + packages/wrangler/src/index.ts | 10 +- .../wrangler/src/workflows/commands/delete.ts | 50 ++--- .../src/workflows/commands/describe.ts | 95 +++++---- .../workflows/commands/instances/describe.ts | 187 +++++++++--------- .../src/workflows/commands/instances/index.ts | 29 --- .../src/workflows/commands/instances/list.ts | 127 ++++++------ .../workflows/commands/instances/terminate.ts | 90 +++++---- .../wrangler/src/workflows/commands/list.ts | 97 +++++---- .../src/workflows/commands/trigger.ts | 83 ++++---- packages/wrangler/src/workflows/index.ts | 26 +++ packages/wrangler/src/workflows/workflows.ts | 53 ----- pnpm-lock.yaml | 16 ++ 14 files changed, 408 insertions(+), 458 deletions(-) delete mode 100644 packages/wrangler/src/workflows/commands/instances/index.ts create mode 100644 packages/wrangler/src/workflows/index.ts delete mode 100644 packages/wrangler/src/workflows/workflows.ts diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 27151af3d295..2ffedcf65f49 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -75,7 +75,9 @@ "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", "chokidar": "^3.5.3", + "date-fns": "^4.1.0", "esbuild": "0.17.19", + "itty-time": "^1.0.6", "miniflare": "workspace:*", "nanoid": "^3.3.3", "path-to-regexp": "^6.3.0", diff --git a/packages/wrangler/src/core/teams.d.ts b/packages/wrangler/src/core/teams.d.ts index 82499b0fab84..204e2312f745 100644 --- a/packages/wrangler/src/core/teams.d.ts +++ b/packages/wrangler/src/core/teams.d.ts @@ -14,4 +14,5 @@ export type Teams = | "Product: AI" | "Product: Hyperdrive" | "Product: Vectorize" + | "Product: Workflows" | "Product: Cloudchamber"; diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index 19d0bd6c3275..458eb44894ad 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -43,6 +43,7 @@ import { generateHandler, generateOptions } from "./generate"; import { hyperdrive } from "./hyperdrive/index"; import { initHandler, initOptions } from "./init"; import "./kv"; +import "./workflows"; import { logBuildFailure, logger, LOGGER_LEVELS } from "./logger"; import * as metrics from "./metrics"; import { mTlsCertificateCommands } from "./mtls-certificate/cli"; @@ -77,7 +78,6 @@ import registerVersionsSubcommands from "./versions"; import registerVersionsDeploymentsSubcommands from "./versions/deployments"; import registerVersionsRollbackCommand from "./versions/rollback"; import { whoami } from "./whoami"; -import { workflows } from "./workflows/workflows"; import { asJson } from "./yargs-types"; import type { Config } from "./config"; import type { LoggerLevel } from "./logger"; @@ -609,16 +609,14 @@ export function createCLIParser(argv: string[]) { return ai(aiYargs.command(subHelp)); }); + // workflows + register.registerNamespace("workflows"); + // pipelines wrangler.command("pipelines", false, (pipelinesYargs) => { return pipelines(pipelinesYargs.command(subHelp)); }); - // workflows - wrangler.command("workflows", "🧑‍🍳 Manage Workflows", (workflowArgs) => { - return workflows(workflowArgs.command(subHelp), subHelp); - }); - /******************** CMD GROUP ***********************/ // login wrangler.command( diff --git a/packages/wrangler/src/workflows/commands/delete.ts b/packages/wrangler/src/workflows/commands/delete.ts index 53cf826f14f7..c5b1e00aa7c9 100644 --- a/packages/wrangler/src/workflows/commands/delete.ts +++ b/packages/wrangler/src/workflows/commands/delete.ts @@ -1,33 +1,33 @@ import { fetchResult } from "../../cfetch"; -import { readConfig } from "../../config"; +import { defineCommand } from "../../core"; import { logger } from "../../logger"; -import { printWranglerBanner } from "../../update-check"; import { requireAuth } from "../../user"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; -export const workflowDeleteOptions = (args: CommonYargsArgv) => { - return args.positional("name", { - describe: "Name of the workflow", - type: "string", - demandOption: true, - }); -}; +defineCommand({ + command: "wrangler workflows delete", + metadata: { + description: + "Delete workflow - when deleting a workflow, it will also delete it's own instances", + owner: "Product: Workflows", + status: "open-beta", + }, -type HandlerOptions = StrictYargsOptionsToInterface< - typeof workflowDeleteOptions ->; -export const workflowDeleteHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); + args: { + name: { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }, + }, + positionalArgs: ["name"], - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); + async handler(args, { config }) { + const accountId = await requireAuth(config); - await fetchResult(`/accounts/${accountId}/workflows/${args.name}`, { - method: "DELETE", - }); + await fetchResult(`/accounts/${accountId}/workflows/${args.name}`, { + method: "DELETE", + }); - logger.info(`Workflow "${args.name}" was successfully removed`); -}; + logger.info(`Workflow "${args.name}" was successfully removed`); + }, +}); diff --git a/packages/wrangler/src/workflows/commands/describe.ts b/packages/wrangler/src/workflows/commands/describe.ts index 36b7ef6d62d4..da7d33564543 100644 --- a/packages/wrangler/src/workflows/commands/describe.ts +++ b/packages/wrangler/src/workflows/commands/describe.ts @@ -1,62 +1,59 @@ import { logRaw } from "@cloudflare/cli"; import { white } from "@cloudflare/cli/colors"; import { fetchResult } from "../../cfetch"; -import { readConfig } from "../../config"; -import { printWranglerBanner } from "../../update-check"; +import { defineCommand } from "../../core"; import { requireAuth } from "../../user"; import formatLabelledValues from "../../utils/render-labelled-values"; -import { - type CommonYargsArgv, - type StrictYargsOptionsToInterface, -} from "../../yargs-types"; import type { Version, Workflow } from "../types"; -export const workflowDescribeOptions = (args: CommonYargsArgv) => { - return args.positional("name", { - describe: "Name of the workflow", - type: "string", - demandOption: true, - }); -}; +defineCommand({ + command: "wrangler workflows describe", + metadata: { + description: "Describe Workflow resource", + owner: "Product: Workflows", + status: "open-beta", + }, + args: { + name: { + describe: "Name of the workflow", + type: "string", + demandOption: true, + }, + }, + positionalArgs: ["name"], + async handler(args, { config }) { + const accountId = await requireAuth(config); -type HandlerOptions = StrictYargsOptionsToInterface< - typeof workflowDescribeOptions ->; -export const workflowDescribeHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); + const workflow = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}` + ); - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); + const versions = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/versions` + ); - const workflow = await fetchResult( - `/accounts/${accountId}/workflows/${args.name}` - ); + const latestVersion = versions[0]; - const versions = await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/versions` - ); - - const latestVersion = versions[0]; - - logRaw( - formatLabelledValues({ - Name: workflow.name, - Id: workflow.id, - "Script Name": workflow.script_name, - "Class Name": workflow.class_name, - "Created On": workflow.created_on, - "Modified On": workflow.modified_on, - }) - ); - logRaw(white("Latest Version:")); - logRaw( - formatLabelledValues( - { - Id: latestVersion.id, + logRaw( + formatLabelledValues({ + Name: workflow.name, + Id: workflow.id, + "Script Name": workflow.script_name, + "Class Name": workflow.class_name, "Created On": workflow.created_on, "Modified On": workflow.modified_on, - }, - { indentationCount: 2 } - ) - ); -}; + }) + ); + logRaw(white("Latest Version:")); + logRaw( + formatLabelledValues( + { + Id: latestVersion.id, + "Created On": workflow.created_on, + "Modified On": workflow.modified_on, + }, + { indentationCount: 2 } + ) + ); + }, +}); diff --git a/packages/wrangler/src/workflows/commands/instances/describe.ts b/packages/wrangler/src/workflows/commands/instances/describe.ts index f227e941d9fe..3ab4d6c061a2 100644 --- a/packages/wrangler/src/workflows/commands/instances/describe.ts +++ b/packages/wrangler/src/workflows/commands/instances/describe.ts @@ -7,9 +7,8 @@ import { } from "date-fns"; import { ms } from "itty-time"; import { fetchResult } from "../../../cfetch"; -import { readConfig } from "../../../config"; +import { defineCommand } from "../../../core"; import { logger } from "../../../logger"; -import { printWranglerBanner } from "../../../update-check"; import { requireAuth } from "../../../user"; import formatLabelledValues from "../../../utils/render-labelled-values"; import { @@ -17,10 +16,6 @@ import { emojifyInstanceTriggerName, emojifyStepType, } from "../../utils"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../../yargs-types"; import type { Instance, InstanceSleepLog, @@ -29,122 +24,126 @@ import type { InstanceTerminateLog, } from "../../types"; -export const instancesDescribeOptions = (args: CommonYargsArgv) => { - return args - .positional("name", { +const command = defineCommand({ + command: "wrangler workflows instances describe", + + metadata: { + description: + "Describe a workflow instance - see its logs, retries and errors", + owner: "Product: Workflows", + status: "open-beta", + }, + + positionalArgs: ["name", "id"], + args: { + name: { describe: "Name of the workflow", type: "string", demandOption: true, - }) - .positional("id", { + }, + id: { describe: "ID of the instance - instead of an UUID you can type 'latest' to get the latest instance and describe it", type: "string", demandOption: true, - }) - .option("step-output", { + }, + "step-output": { describe: "Don't output the step output since it might clutter the terminal", type: "boolean", default: true, - }) - .option("truncate-output-limit", { + }, + "truncate-output-limit": { describe: "Truncate step output after x characters", type: "number", default: 5000, - }); -}; + }, + }, -type HandlerOptions = StrictYargsOptionsToInterface< - typeof instancesDescribeOptions ->; + async handler(args, { config }) { + const accountId = await requireAuth(config); -export const instancesDescribeHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); + let id = args.id; - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); + if (id == "latest") { + const instances = ( + await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances` + ) + ).sort((a, b) => b.created_on.localeCompare(a.created_on)); - let id = args.id; - - if (id == "latest") { - const instances = ( - await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/instances` - ) - ).sort((a, b) => b.created_on.localeCompare(a.created_on)); + if (instances.length == 0) { + logger.error( + `There are no deployed instances in workflow "${args.name}".` + ); + return; + } - if (instances.length == 0) { - logger.error( - `There are no deployed instances in workflow "${args.name}".` - ); - return; + id = instances[0].id; } - id = instances[0].id; - } + const instance = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances/${id}` + ); - const instance = await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/instances/${id}` - ); - - const formattedInstance: Record = { - "Workflow Name": args.name, - "Instance Id": id, - "Version Id": instance.versionId, - Status: emojifyInstanceStatus(instance.status), - Trigger: emojifyInstanceTriggerName(instance.trigger.source), - Queued: new Date(instance.queued).toLocaleString(), - }; - - if (instance.success != null) { - formattedInstance.Success = instance.success ? "✅ Yes" : "❌ No"; - } + const formattedInstance: Record = { + "Workflow Name": args.name, + "Instance Id": id, + "Version Id": instance.versionId, + Status: emojifyInstanceStatus(instance.status), + Trigger: emojifyInstanceTriggerName(instance.trigger.source), + Queued: new Date(instance.queued).toLocaleString(), + }; + + if (instance.success != null) { + formattedInstance.Success = instance.success ? "✅ Yes" : "❌ No"; + } - // date related stuff, if the workflow is still running assume duration until now - if (instance.start != undefined) { - formattedInstance.Start = new Date(instance.start).toLocaleString(); - } + // date related stuff, if the workflow is still running assume duration until now + if (instance.start != undefined) { + formattedInstance.Start = new Date(instance.start).toLocaleString(); + } - if (instance.end != undefined) { - formattedInstance.End = new Date(instance.end).toLocaleString(); - } + if (instance.end != undefined) { + formattedInstance.End = new Date(instance.end).toLocaleString(); + } - if (instance.start != null && instance.end != null) { - formattedInstance.Duration = formatDistanceStrict( - new Date(instance.end), - new Date(instance.start) - ); - } else if (instance.start != null) { - // Convert current date to UTC - formattedInstance.Duration = formatDistanceStrict( - new Date(instance.start), - new Date(new Date().toUTCString().slice(0, -4)) - ); - } + if (instance.start != null && instance.end != null) { + formattedInstance.Duration = formatDistanceStrict( + new Date(instance.end), + new Date(instance.start) + ); + } else if (instance.start != null) { + // Convert current date to UTC + formattedInstance.Duration = formatDistanceStrict( + new Date(instance.start), + new Date(new Date().toUTCString().slice(0, -4)) + ); + } - const lastSuccessfulStepName = getLastSuccessfulStep(instance); - if (lastSuccessfulStepName != null) { - formattedInstance["Last Successful Step"] = lastSuccessfulStepName; - } + const lastSuccessfulStepName = getLastSuccessfulStep(instance); + if (lastSuccessfulStepName != null) { + formattedInstance["Last Successful Step"] = lastSuccessfulStepName; + } - // display the error if the instance errored out - if (instance.error != null) { - formattedInstance.Error = red( - `${instance.error.name}: ${instance.error.message}` - ); - } + // display the error if the instance errored out + if (instance.error != null) { + formattedInstance.Error = red( + `${instance.error.name}: ${instance.error.message}` + ); + } - logRaw(formatLabelledValues(formattedInstance)); - logRaw(white("Steps:")); + logRaw(formatLabelledValues(formattedInstance)); + logRaw(white("Steps:")); - instance.steps.forEach(logStep.bind(false, args)); -}; + instance.steps.forEach(logStep.bind(false, args)); + }, +}); -const logStep = ( - args: HandlerOptions, +function logStep( + args: typeof command.args, step: InstanceStepLog | InstanceSleepLog | InstanceTerminateLog -) => { +) { logRaw(""); const formattedStep: Record = {}; @@ -254,9 +253,9 @@ const logStep = ( logger.table(prettyAttempts); } -}; +} -const getLastSuccessfulStep = (logs: InstanceStatusAndLogs): string | null => { +function getLastSuccessfulStep(logs: InstanceStatusAndLogs): string | null { let lastSuccessfulStepName: string | null = null; for (const step of logs.steps) { @@ -277,4 +276,4 @@ const getLastSuccessfulStep = (logs: InstanceStatusAndLogs): string | null => { } return lastSuccessfulStepName; -}; +} diff --git a/packages/wrangler/src/workflows/commands/instances/index.ts b/packages/wrangler/src/workflows/commands/instances/index.ts deleted file mode 100644 index 51e29a8c03db..000000000000 --- a/packages/wrangler/src/workflows/commands/instances/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { instancesDescribeHandler, instancesDescribeOptions } from "./describe"; -import { instancesListHandler, instancesListOptions } from "./list"; -import { - instancesTerminateHandler, - instancesTerminateOptions, -} from "./terminate"; -import type { CommonYargsArgv } from "../../../yargs-types"; - -export const instances = (args: CommonYargsArgv) => { - return args - .command( - "list ", - "List workflow instances", - instancesListOptions, - instancesListHandler - ) - .command( - "describe ", - "Describe a workflow instance - see its logs, retries and errors", - instancesDescribeOptions, - instancesDescribeHandler - ) - .command( - "terminate ", - "Terminate a workflow instance", - instancesTerminateOptions, - instancesTerminateHandler - ); -}; diff --git a/packages/wrangler/src/workflows/commands/instances/list.ts b/packages/wrangler/src/workflows/commands/instances/list.ts index 652402a75def..2b30695786ef 100644 --- a/packages/wrangler/src/workflows/commands/instances/list.ts +++ b/packages/wrangler/src/workflows/commands/instances/list.ts @@ -1,95 +1,94 @@ -import { printWranglerBanner } from "../../.."; import { fetchResult } from "../../../cfetch"; -import { readConfig } from "../../../config"; +import { defineCommand } from "../../../core"; import { logger } from "../../../logger"; import { requireAuth } from "../../../user"; import { emojifyInstanceStatus, validateStatus } from "../../utils"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../../yargs-types"; import type { Instance } from "../../types"; -export const instancesListOptions = (args: CommonYargsArgv) => { - return args - .positional("name", { +defineCommand({ + command: "wrangler workflows instances list", + + metadata: { + description: "Instance related commands (list, describe, terminate...)", + owner: "Product: Workflows", + status: "open-beta", + }, + + positionalArgs: ["name"], + args: { + name: { describe: "Name of the workflow", type: "string", demandOption: true, - }) - .option("reverse", { + }, + reverse: { describe: "Reverse order of the instances table", type: "boolean", default: false, - }) - .option("status", { + }, + status: { describe: "Filters list by instance status (can be one of: queued, running, paused, errored, terminated, complete)", type: "string", - }) - .option("page", { + }, + page: { describe: 'Show a sepecific page from the listing, can configure page size using "per-page".', type: "number", default: 1, - }) - .option("per-page", { + }, + "per-page": { describe: "Configure the maximum number of instances to show per page.", type: "number", - }); -}; + }, + }, -type HandlerOptions = StrictYargsOptionsToInterface< - typeof instancesListOptions ->; -export const instancesListHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); + async handler(args, { config }) { + const accountId = await requireAuth(config); - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); + const URLParams = new URLSearchParams(); - const URLParams = new URLSearchParams(); + if (args.status !== undefined) { + const validatedStatus = validateStatus(args.status); + URLParams.set("status", validatedStatus); + } + if (args.perPage !== undefined) { + URLParams.set("per_page", args.perPage.toString()); + } - if (args.status !== undefined) { - const validatedStatus = validateStatus(args.status); - URLParams.set("status", validatedStatus); - } - if (args.perPage !== undefined) { - URLParams.set("per_page", args.perPage.toString()); - } + URLParams.set("page", args.page.toString()); - URLParams.set("page", args.page.toString()); + const instances = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances`, + undefined, + URLParams + ); - const instances = await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/instances`, - undefined, - URLParams - ); + if (instances.length === 0) { + logger.warn( + `There are no instances in workflow "${args.name}". You can trigger it with "wrangler workflows trigger ${args.name}"` + ); + return; + } - if (instances.length === 0) { - logger.warn( - `There are no instances in workflow "${args.name}". You can trigger it with "wrangler workflows trigger ${args.name}"` + logger.info( + `Showing ${instances.length} instance${instances.length > 1 ? "s" : ""} from page ${args.page}:` ); - return; - } - - logger.info( - `Showing ${instances.length} instance${instances.length > 1 ? "s" : ""} from page ${args.page}:` - ); - const prettierInstances = instances - .sort((a, b) => - args.reverse - ? a.modified_on.localeCompare(b.modified_on) - : b.modified_on.localeCompare(a.modified_on) - ) - .map((instance) => ({ - Id: instance.id, - Version: instance.version_id, - Created: new Date(instance.created_on).toLocaleString(), - Modified: new Date(instance.modified_on).toLocaleString(), - Status: emojifyInstanceStatus(instance.status), - })); + const prettierInstances = instances + .sort((a, b) => + args.reverse + ? a.modified_on.localeCompare(b.modified_on) + : b.modified_on.localeCompare(a.modified_on) + ) + .map((instance) => ({ + Id: instance.id, + Version: instance.version_id, + Created: new Date(instance.created_on).toLocaleString(), + Modified: new Date(instance.modified_on).toLocaleString(), + Status: emojifyInstanceStatus(instance.status), + })); - logger.table(prettierInstances); -}; + logger.table(prettierInstances); + }, +}); diff --git a/packages/wrangler/src/workflows/commands/instances/terminate.ts b/packages/wrangler/src/workflows/commands/instances/terminate.ts index 543d182b5780..be830530df36 100644 --- a/packages/wrangler/src/workflows/commands/instances/terminate.ts +++ b/packages/wrangler/src/workflows/commands/instances/terminate.ts @@ -1,66 +1,64 @@ import { fetchResult } from "../../../cfetch"; -import { readConfig } from "../../../config"; +import { defineCommand } from "../../../core"; import { logger } from "../../../logger"; -import { printWranglerBanner } from "../../../update-check"; import { requireAuth } from "../../../user"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../../yargs-types"; import type { Instance } from "../../types"; -export const instancesTerminateOptions = (args: CommonYargsArgv) => { - return args - .positional("name", { +defineCommand({ + command: "wrangler workflows instances terminate", + + metadata: { + description: "Terminate a workflow instance", + owner: "Product: Workflows", + status: "open-beta", + }, + + positionalArgs: ["name", "id"], + args: { + name: { describe: "Name of the workflow", type: "string", demandOption: true, - }) - .positional("id", { + }, + id: { describe: "ID of the instance - instead of an UUID you can type 'latest' to get the latest instance and describe it", type: "string", demandOption: true, - }); -}; - -type HandlerOptions = StrictYargsOptionsToInterface< - typeof instancesTerminateOptions ->; + }, + }, -export const instancesTerminateHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); + async handler(args, { config }) { + const accountId = await requireAuth(config); - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); + let id = args.id; - let id = args.id; + if (id == "latest") { + const instances = ( + await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances` + ) + ).sort((a, b) => b.created_on.localeCompare(a.created_on)); - if (id == "latest") { - const instances = ( - await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/instances` - ) - ).sort((a, b) => b.created_on.localeCompare(a.created_on)); + if (instances.length == 0) { + logger.error( + `There are no deployed instances in workflow "${args.name}".` + ); + return; + } - if (instances.length == 0) { - logger.error( - `There are no deployed instances in workflow "${args.name}".` - ); - return; + id = instances[0].id; } - id = instances[0].id; - } - - await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/instances/${id}`, - { - method: "DELETE", - } - ); + await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances/${id}`, + { + method: "DELETE", + } + ); - logger.info( - `🥷 The instance "${id}" from ${args.name} was terminated successfully.` - ); -}; + logger.info( + `🥷 The instance "${id}" from ${args.name} was terminated successfully.` + ); + }, +}); diff --git a/packages/wrangler/src/workflows/commands/list.ts b/packages/wrangler/src/workflows/commands/list.ts index 13d3ca1961fe..bc8deca6d641 100644 --- a/packages/wrangler/src/workflows/commands/list.ts +++ b/packages/wrangler/src/workflows/commands/list.ts @@ -1,66 +1,63 @@ -import { printWranglerBanner } from "../.."; import { fetchResult } from "../../cfetch"; -import { readConfig } from "../../config"; +import { defineCommand } from "../../core"; import { logger } from "../../logger"; import { requireAuth } from "../../user"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; import type { Workflow } from "../types"; -export const workflowListOptions = (args: CommonYargsArgv) => { - return args - .option("page", { +defineCommand({ + command: "wrangler workflows list", + metadata: { + description: "List Workflows associated to account", + owner: "Product: Workflows", + status: "open-beta", + }, + args: { + page: { describe: 'Show a sepecific page from the listing, can configure page size using "per-page".', type: "number", default: 1, - }) - .option("per-page", { + }, + "per-page": { describe: "Configure the maximum number of workflows to show per page.", type: "number", - }); -}; + }, + }, + async handler(args, { config }) { + const accountId = await requireAuth(config); -type HandlerOptions = StrictYargsOptionsToInterface; -export const workflowListHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); + const URLParams = new URLSearchParams(); - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); + if (args.perPage !== undefined) { + URLParams.set("per_page", args.perPage.toString()); + } - const URLParams = new URLSearchParams(); + URLParams.set("page", args.page.toString()); - if (args.perPage !== undefined) { - URLParams.set("per_page", args.perPage.toString()); - } - - URLParams.set("page", args.page.toString()); - - const workflows = await fetchResult( - `/accounts/${accountId}/workflows`, - undefined, - URLParams - ); - - if (workflows.length === 0) { - logger.warn("There are no deployed Workflows in this account."); - } else { - // TODO(lduarte): can we improve this message once pagination is deployed - logger.info( - `Showing last ${workflows.length} workflow${workflows.length > 1 ? "s" : ""}:` + const workflows = await fetchResult( + `/accounts/${accountId}/workflows`, + undefined, + URLParams ); - // sort by name and make the table head prettier by changing the keys - const prettierWorkflows = workflows - .sort((a, b) => a.name.localeCompare(b.name)) - .map((workflow) => ({ - Name: workflow.name, - "Script name": workflow.script_name, - "Class name": workflow.class_name, - Created: new Date(workflow.created_on).toLocaleString(), - Modified: new Date(workflow.modified_on).toLocaleString(), - })); - logger.table(prettierWorkflows); - } -}; + + if (workflows.length === 0) { + logger.warn("There are no deployed Workflows in this account."); + } else { + // TODO(lduarte): can we improve this message once pagination is deployed + logger.info( + `Showing last ${workflows.length} workflow${workflows.length > 1 ? "s" : ""}:` + ); + // sort by name and make the table head prettier by changing the keys + const prettierWorkflows = workflows + .sort((a, b) => a.name.localeCompare(b.name)) + .map((workflow) => ({ + Name: workflow.name, + "Script name": workflow.script_name, + "Class name": workflow.class_name, + Created: new Date(workflow.created_on).toLocaleString(), + Modified: new Date(workflow.modified_on).toLocaleString(), + })); + logger.table(prettierWorkflows); + } + }, +}); diff --git a/packages/wrangler/src/workflows/commands/trigger.ts b/packages/wrangler/src/workflows/commands/trigger.ts index aa087a9fc80f..561d47d35fdd 100644 --- a/packages/wrangler/src/workflows/commands/trigger.ts +++ b/packages/wrangler/src/workflows/commands/trigger.ts @@ -1,57 +1,56 @@ import { fetchResult } from "../../cfetch"; -import { readConfig } from "../../config"; -import { logger } from "../../logger"; -import { printWranglerBanner } from "../../update-check"; +import { defineCommand } from "../../core"; import { requireAuth } from "../../user"; -import type { - CommonYargsArgv, - StrictYargsOptionsToInterface, -} from "../../yargs-types"; import type { InstanceWithoutDates } from "../types"; -export const workflowTriggerOptions = (args: CommonYargsArgv) => { - return args - .positional("name", { +defineCommand({ + command: "wrangler workflows trigger", + + metadata: { + description: + "Trigger a workflow, creating a new instance. Can optionally take a JSON string to pass a parameter into the workflow instance.", + owner: "Product: Workflows", + status: "open-beta", + }, + + args: { + name: { describe: "Name of the workflow", type: "string", demandOption: true, - }) - .positional("params", { + }, + params: { describe: "Params for the workflow instance, encoded as a JSON string", type: "string", default: "", - }); -}; + }, + }, + positionalArgs: ["name", "params"], -type HandlerOptions = StrictYargsOptionsToInterface< - typeof workflowTriggerOptions ->; -export const workflowTriggerHandler = async (args: HandlerOptions) => { - await printWranglerBanner(); + async handler(args, { config, logger }) { + const accountId = await requireAuth(config); - const config = readConfig(args.config, args); - const accountId = await requireAuth(config); - - if (args.params.length != 0) { - try { - JSON.parse(args.params); - } catch (e) { - logger.error( - `Error while parsing instance parameters: "${args.params}" with ${e}' ` - ); - return; + if (args.params.length != 0) { + try { + JSON.parse(args.params); + } catch (e) { + logger.error( + `Error while parsing instance parameters: "${args.params}" with ${e}' ` + ); + return; + } } - } - const response = await fetchResult( - `/accounts/${accountId}/workflows/${args.name}/instances`, - { - method: "POST", - body: args.params.length != 0 ? args.params : undefined, - } - ); + const response = await fetchResult( + `/accounts/${accountId}/workflows/${args.name}/instances`, + { + method: "POST", + body: args.params.length != 0 ? args.params : undefined, + } + ); - logger.info( - `🚀 Workflow instance "${response.id}" has been queued successfully` - ); -}; + logger.info( + `🚀 Workflow instance "${response.id}" has been queued successfully` + ); + }, +}); diff --git a/packages/wrangler/src/workflows/index.ts b/packages/wrangler/src/workflows/index.ts new file mode 100644 index 000000000000..5a20e510d89c --- /dev/null +++ b/packages/wrangler/src/workflows/index.ts @@ -0,0 +1,26 @@ +import { defineNamespace } from "../core"; +import "./commands/list"; +import "./commands/describe"; +import "./commands/delete"; +import "./commands/trigger"; +import "./commands/instances/list"; +import "./commands/instances/describe"; +import "./commands/instances/terminate"; + +defineNamespace({ + command: "wrangler workflows", + metadata: { + description: "🔁 Manage Workflows", + owner: "Product: Workflows", + status: "open-beta", + }, +}); + +defineNamespace({ + command: "wrangler workflows instances", + metadata: { + description: "Manage Workflow instances", + owner: "Product: Workflows", + status: "open-beta", + }, +}); diff --git a/packages/wrangler/src/workflows/workflows.ts b/packages/wrangler/src/workflows/workflows.ts deleted file mode 100644 index a8e5310a7371..000000000000 --- a/packages/wrangler/src/workflows/workflows.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { type CommonYargsArgv, type SubHelp } from "../yargs-types"; -import { - workflowDeleteHandler, - workflowDeleteOptions, -} from "./commands/delete"; -import { - workflowDescribeHandler, - workflowDescribeOptions, -} from "./commands/describe"; -import { instances } from "./commands/instances"; -import { workflowListHandler, workflowListOptions } from "./commands/list"; -import { - workflowTriggerHandler, - workflowTriggerOptions, -} from "./commands/trigger"; - -const workflowsEpilog = "🚨 Workflows is currently in Open Beta."; - -export const workflows = (yargs: CommonYargsArgv, subHelp: SubHelp) => { - return yargs - .command( - "list", - "List Workflows associated to account", - workflowListOptions, - workflowListHandler - ) - .command( - "describe ", - "Describe Workflow resource", - workflowDescribeOptions, - workflowDescribeHandler - ) - .command( - "delete ", - "Delete workflow - when deleting a workflow, it will also delete it's own instances", - workflowDeleteOptions, - workflowDeleteHandler - ) - .command( - "trigger [params]", - "Trigger a workflow, creating a new instance. Can optionally take a JSON string to pass a parameter into the workflow instance.", - workflowTriggerOptions, - workflowTriggerHandler - ) - .command( - "instances", - "Instance related commands (list, describe, terminate...)", - (instancesYargs) => { - return instances(instancesYargs).command(subHelp); - } - ) - .epilog(workflowsEpilog); -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e61acb2d84c..0beb9861ac1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1647,9 +1647,15 @@ importers: chokidar: specifier: ^3.5.3 version: 3.5.3 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 esbuild: specifier: 0.17.19 version: 0.17.19 + itty-time: + specifier: ^1.0.6 + version: 1.0.6 miniflare: specifier: workspace:* version: link:../miniflare @@ -4593,6 +4599,9 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + date-time@3.1.0: resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} engines: {node: '>=6'} @@ -5918,6 +5927,9 @@ packages: itty-router@4.0.17: resolution: {integrity: sha512-Fu/GO3MFX6Hwd+QF1/BFjoFpPQKh2Bu4DtBdse5kECcUwldNBrZqgwq0IariLrP67iADGlzkHIlOWwJ8F4SJ4A==} + itty-time@1.0.6: + resolution: {integrity: sha512-+P8IZaLLBtFv8hCkIjcymZOp4UJ+xW6bSlQsXGqrkmJh7vSiMFSlNne0mCYagEE0N7HDNR5jJBRxwN0oYv61Rw==} + jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} @@ -11548,6 +11560,8 @@ snapshots: dependencies: '@babel/runtime': 7.22.5 + date-fns@4.1.0: {} + date-time@3.1.0: dependencies: time-zone: 1.0.0 @@ -13113,6 +13127,8 @@ snapshots: itty-router@4.0.17: {} + itty-time@1.0.6: {} + jackspeak@2.3.6: dependencies: '@isaacs/cliui': 8.0.2 From 74a8019dccda60d4642e5c3644eb732b66fae734 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Fri, 18 Oct 2024 02:10:45 +0100 Subject: [PATCH 13/16] add changeset --- .changeset/moody-seals-smash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/moody-seals-smash.md diff --git a/.changeset/moody-seals-smash.md b/.changeset/moody-seals-smash.md new file mode 100644 index 000000000000..0445665d00a4 --- /dev/null +++ b/.changeset/moody-seals-smash.md @@ -0,0 +1,5 @@ +--- +"wrangler": minor +--- + +add `wrangler workflows ...` commands From 5f368a07ca36218c717d736e7c3b067a2560fbc4 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Fri, 18 Oct 2024 02:20:08 +0100 Subject: [PATCH 14/16] update snapshots --- .../wrangler/src/__tests__/core/command-registration.test.ts | 1 + packages/wrangler/src/__tests__/index.test.ts | 2 ++ packages/wrangler/src/__tests__/versions/versions.help.test.ts | 1 + 3 files changed, 4 insertions(+) diff --git a/packages/wrangler/src/__tests__/core/command-registration.test.ts b/packages/wrangler/src/__tests__/core/command-registration.test.ts index 12f24a560120..538b38761cc4 100644 --- a/packages/wrangler/src/__tests__/core/command-registration.test.ts +++ b/packages/wrangler/src/__tests__/core/command-registration.test.ts @@ -191,6 +191,7 @@ describe("Command Registration", () => { wrangler pubsub 📮 Manage Pub/Sub brokers [private beta] wrangler dispatch-namespace 🏗️ Manage dispatch namespaces wrangler ai 🤖 Manage AI models + wrangler workflows 🔁 Manage Workflows [open-beta] wrangler login 🔓 Login to Cloudflare wrangler logout 🚪 Logout from Cloudflare wrangler whoami 🕵️ Retrieve your user information diff --git a/packages/wrangler/src/__tests__/index.test.ts b/packages/wrangler/src/__tests__/index.test.ts index 49c9b2aaed2d..ac55c7326f60 100644 --- a/packages/wrangler/src/__tests__/index.test.ts +++ b/packages/wrangler/src/__tests__/index.test.ts @@ -61,6 +61,7 @@ describe("wrangler", () => { wrangler pubsub 📮 Manage Pub/Sub brokers [private beta] wrangler dispatch-namespace 🏗️ Manage dispatch namespaces wrangler ai 🤖 Manage AI models + wrangler workflows 🔁 Manage Workflows [open-beta] wrangler login 🔓 Login to Cloudflare wrangler logout 🚪 Logout from Cloudflare wrangler whoami 🕵️ Retrieve your user information @@ -117,6 +118,7 @@ describe("wrangler", () => { wrangler pubsub 📮 Manage Pub/Sub brokers [private beta] wrangler dispatch-namespace 🏗️ Manage dispatch namespaces wrangler ai 🤖 Manage AI models + wrangler workflows 🔁 Manage Workflows [open-beta] wrangler login 🔓 Login to Cloudflare wrangler logout 🚪 Logout from Cloudflare wrangler whoami 🕵️ Retrieve your user information diff --git a/packages/wrangler/src/__tests__/versions/versions.help.test.ts b/packages/wrangler/src/__tests__/versions/versions.help.test.ts index 859c2c5886f7..1986212e95d6 100644 --- a/packages/wrangler/src/__tests__/versions/versions.help.test.ts +++ b/packages/wrangler/src/__tests__/versions/versions.help.test.ts @@ -37,6 +37,7 @@ describe("versions --help", () => { wrangler pubsub 📮 Manage Pub/Sub brokers [private beta] wrangler dispatch-namespace 🏗️ Manage dispatch namespaces wrangler ai 🤖 Manage AI models + wrangler workflows 🔁 Manage Workflows [open-beta] wrangler login 🔓 Login to Cloudflare wrangler logout 🚪 Logout from Cloudflare wrangler whoami 🕵️ Retrieve your user information From 9a2b2e42b52bbbd09d6001fe834d26f948dbfaef Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:13:48 +0100 Subject: [PATCH 15/16] remove extraneous periods --- .../wrangler/src/workflows/commands/instances/describe.ts | 2 +- packages/wrangler/src/workflows/commands/instances/list.ts | 4 ++-- .../wrangler/src/workflows/commands/instances/terminate.ts | 4 ++-- packages/wrangler/src/workflows/commands/list.ts | 6 +++--- packages/wrangler/src/workflows/commands/trigger.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/wrangler/src/workflows/commands/instances/describe.ts b/packages/wrangler/src/workflows/commands/instances/describe.ts index 3ab4d6c061a2..f7327e0661a3 100644 --- a/packages/wrangler/src/workflows/commands/instances/describe.ts +++ b/packages/wrangler/src/workflows/commands/instances/describe.ts @@ -74,7 +74,7 @@ const command = defineCommand({ if (instances.length == 0) { logger.error( - `There are no deployed instances in workflow "${args.name}".` + `There are no deployed instances in workflow "${args.name}"` ); return; } diff --git a/packages/wrangler/src/workflows/commands/instances/list.ts b/packages/wrangler/src/workflows/commands/instances/list.ts index 2b30695786ef..8ad1166d41c7 100644 --- a/packages/wrangler/src/workflows/commands/instances/list.ts +++ b/packages/wrangler/src/workflows/commands/instances/list.ts @@ -33,12 +33,12 @@ defineCommand({ }, page: { describe: - 'Show a sepecific page from the listing, can configure page size using "per-page".', + 'Show a sepecific page from the listing, can configure page size using "per-page"', type: "number", default: 1, }, "per-page": { - describe: "Configure the maximum number of instances to show per page.", + describe: "Configure the maximum number of instances to show per page", type: "number", }, }, diff --git a/packages/wrangler/src/workflows/commands/instances/terminate.ts b/packages/wrangler/src/workflows/commands/instances/terminate.ts index be830530df36..c82c460b43ee 100644 --- a/packages/wrangler/src/workflows/commands/instances/terminate.ts +++ b/packages/wrangler/src/workflows/commands/instances/terminate.ts @@ -42,7 +42,7 @@ defineCommand({ if (instances.length == 0) { logger.error( - `There are no deployed instances in workflow "${args.name}".` + `There are no deployed instances in workflow "${args.name}"` ); return; } @@ -58,7 +58,7 @@ defineCommand({ ); logger.info( - `🥷 The instance "${id}" from ${args.name} was terminated successfully.` + `🥷 The instance "${id}" from ${args.name} was terminated successfully` ); }, }); diff --git a/packages/wrangler/src/workflows/commands/list.ts b/packages/wrangler/src/workflows/commands/list.ts index bc8deca6d641..2f33d4e9552f 100644 --- a/packages/wrangler/src/workflows/commands/list.ts +++ b/packages/wrangler/src/workflows/commands/list.ts @@ -14,12 +14,12 @@ defineCommand({ args: { page: { describe: - 'Show a sepecific page from the listing, can configure page size using "per-page".', + 'Show a sepecific page from the listing, can configure page size using "per-page"', type: "number", default: 1, }, "per-page": { - describe: "Configure the maximum number of workflows to show per page.", + describe: "Configure the maximum number of workflows to show per page", type: "number", }, }, @@ -41,7 +41,7 @@ defineCommand({ ); if (workflows.length === 0) { - logger.warn("There are no deployed Workflows in this account."); + logger.warn("There are no deployed Workflows in this account"); } else { // TODO(lduarte): can we improve this message once pagination is deployed logger.info( diff --git a/packages/wrangler/src/workflows/commands/trigger.ts b/packages/wrangler/src/workflows/commands/trigger.ts index 561d47d35fdd..6c3e6abcfac2 100644 --- a/packages/wrangler/src/workflows/commands/trigger.ts +++ b/packages/wrangler/src/workflows/commands/trigger.ts @@ -8,7 +8,7 @@ defineCommand({ metadata: { description: - "Trigger a workflow, creating a new instance. Can optionally take a JSON string to pass a parameter into the workflow instance.", + "Trigger a workflow, creating a new instance. Can optionally take a JSON string to pass a parameter into the workflow instance", owner: "Product: Workflows", status: "open-beta", }, From fd82b95a231d5ec2c6209f2e6b9bf9fc14388af8 Mon Sep 17 00:00:00 2001 From: Rahul Sethi <5822355+RamIdeas@users.noreply.github.com> Date: Fri, 18 Oct 2024 18:02:28 +0100 Subject: [PATCH 16/16] remove redundant file --- fixtures/workflow/CHANGELOG.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 fixtures/workflow/CHANGELOG.md diff --git a/fixtures/workflow/CHANGELOG.md b/fixtures/workflow/CHANGELOG.md deleted file mode 100644 index 912ae7b44279..000000000000 --- a/fixtures/workflow/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# worker-ts - -## 0.0.1 - -### Patch Changes - -- [#3588](https://github.com/cloudflare/workers-sdk/pull/3588) [`64631d8b`](https://github.com/cloudflare/workers-sdk/commit/64631d8b59572f49d65325d8f6fec098c5e912b9) Thanks [@penalosa](https://github.com/penalosa)! - fix: Preserve email handlers when applying middleware to user workers.