Skip to content

Commit

Permalink
add commands
Browse files Browse the repository at this point in the history
  • Loading branch information
RamIdeas committed Oct 18, 2024
1 parent 9f6adc9 commit 80af061
Show file tree
Hide file tree
Showing 11 changed files with 818 additions and 0 deletions.
6 changes: 6 additions & 0 deletions packages/wrangler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(
Expand Down
33 changes: 33 additions & 0 deletions packages/wrangler/src/workflows/commands/delete.ts
Original file line number Diff line number Diff line change
@@ -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`);
};
62 changes: 62 additions & 0 deletions packages/wrangler/src/workflows/commands/describe.ts
Original file line number Diff line number Diff line change
@@ -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<Workflow>(
`/accounts/${accountId}/workflows/${args.name}`
);

const versions = await fetchResult<Version[]>(
`/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 }
)
);
};
280 changes: 280 additions & 0 deletions packages/wrangler/src/workflows/commands/instances/describe.ts
Original file line number Diff line number Diff line change
@@ -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<Instance[]>(
`/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<InstanceStatusAndLogs>(
`/accounts/${accountId}/workflows/${args.name}/instances/${id}`
);

const formattedInstance: Record<string, string> = {
"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<string, string> = {};

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<string, string> = {};

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;
};
Loading

0 comments on commit 80af061

Please sign in to comment.