Skip to content

Commit

Permalink
fix: better and more consistent error handling in CLI commands
Browse files Browse the repository at this point in the history
  • Loading branch information
edvald committed May 24, 2018
1 parent 8e4d6d2 commit 36ba7b7
Show file tree
Hide file tree
Showing 40 changed files with 341 additions and 154 deletions.
35 changes: 23 additions & 12 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ import {
Parameter,
StringParameter,
EnvironmentOption,
CommandResult,
} from "./commands/base"
import { ValidateCommand } from "./commands/validate"
import { InternalError, PluginError } from "./exceptions"
import {
GardenError,
InternalError,
PluginError,
} from "./exceptions"
import { Garden } from "./garden"
import { FileWriter } from "./logger/writers"
import { getLogger, RootLogNode } from "./logger"
Expand Down Expand Up @@ -139,7 +144,7 @@ interface OptConfig {
export interface ParseResults {
argv: any
code: number
errors: Error[]
errors: (GardenError | Error)[]
}

interface SywacParseResults extends ParseResults {
Expand Down Expand Up @@ -320,7 +325,6 @@ export class GardenCli {

// We attach the action result to cli context so that we can process it in the parse method
cliContext.details.result = result

}

// Command specific positional args and options are set inside the builder function
Expand All @@ -342,25 +346,30 @@ export class GardenCli {

async parse(): Promise<ParseResults> {
const parseResult: SywacParseResults = await this.program.parse()
const { argv, details, errors, code, output: cliOutput } = parseResult
const { result } = details
const { argv, details, errors: parseErrors, output: cliOutput } = parseResult
const commandResult: CommandResult = details.result
const { output } = argv

let code = parseResult.code

// --help or --version options were called so we log the cli output and exit
if (cliOutput && errors.length < 1) {
if (cliOutput && parseErrors.length < 1) {
this.logger.stop()
console.log(cliOutput)
process.exit(parseResult.code)
}

// --output option set and there is content to output
if (output && (result !== undefined || errors.length > 0)) {
const errors: GardenError[] = parseErrors
.map(e => ({ type: "parameter", message: e.toString() }))
.concat(commandResult.errors || [])

// --output option set
if (output) {
const renderer = OUTPUT_RENDERERS[output]
if (errors.length > 0) {
const msg = errors.join("\n")
console.error(renderer({ result: msg }))
console.error(renderer({ success: false, errors }))
} else {
console.log(renderer({ success: true, result }))
console.log(renderer({ success: true, ...commandResult }))
}
// Note: this circumvents an issue where the process exits before the output is fully flushed
await sleep(100)
Expand All @@ -370,8 +379,10 @@ export class GardenCli {
errors.forEach(err => this.logger.error({ msg: err.message, error: err }))

if (this.logger.writers.find(w => w instanceof FileWriter)) {
this.logger.info(`See ${ERROR_LOG_FILENAME} for detailed error message`)
this.logger.info(`\nSee ${ERROR_LOG_FILENAME} for detailed error message`)
}

code = 1
}

this.logger.stop()
Expand Down
29 changes: 28 additions & 1 deletion src/commands/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import {
GardenError,
RuntimeError,
} from "../exceptions"
import { PluginContext } from "../plugin-context"
import { TaskResults } from "../task-graph"

export class ValidationError extends Error { }

Expand Down Expand Up @@ -120,6 +125,11 @@ export interface CommandConstructor {
new(parent?: Command): Command
}

export interface CommandResult<T = any> {
result?: T
errors?: GardenError[]
}

export abstract class Command<T extends Parameters = {}, U extends Parameters = {}> {
abstract name: string
abstract help: string
Expand All @@ -141,5 +151,22 @@ export abstract class Command<T extends Parameters = {}, U extends Parameters =
// subclass implementations need to explicitly set the types in the implemented function signature. So for now we
// can't enforce the types of `args` and `opts` automatically at the abstract class level and have to specify
// the types explicitly on the subclassed methods.
abstract async action(ctx: PluginContext, args: ParameterValues<T>, opts: ParameterValues<U>): Promise<any>
abstract async action(ctx: PluginContext, args: ParameterValues<T>, opts: ParameterValues<U>): Promise<CommandResult>
}

export async function handleTaskResults(
ctx: PluginContext, taskType: string, result: TaskResults,
): Promise<CommandResult<TaskResults>> {
const failed = Object.values(result).filter(r => !!r.error).length

if (failed) {
const error = new RuntimeError(`${failed} ${taskType} task(s) failed!`, {
result,
})
return { errors: [error] }
} else {
ctx.log.info("")
ctx.log.header({ emoji: "heavy_check_mark", command: `Done!` })
return { result }
}
}
20 changes: 12 additions & 8 deletions src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@
*/

import { PluginContext } from "../plugin-context"
import { BooleanParameter, Command, ParameterValues, StringParameter } from "./base"
import {
BooleanParameter,
Command,
CommandResult,
handleTaskResults,
ParameterValues,
StringParameter,
} from "./base"
import { BuildTask } from "../tasks/build"
import { TaskResults } from "../task-graph"

Expand All @@ -32,24 +39,21 @@ export class BuildCommand extends Command<typeof buildArguments, typeof buildOpt
arguments = buildArguments
options = buildOptions

async action(ctx: PluginContext, args: BuildArguments, opts: BuildOptions): Promise<TaskResults> {
async action(ctx: PluginContext, args: BuildArguments, opts: BuildOptions): Promise<CommandResult<TaskResults>> {
await ctx.clearBuilds()
const names = args.module ? args.module.split(",") : undefined
const modules = await ctx.getModules(names)

ctx.log.header({ emoji: "hammer", command: "build" })
ctx.log.header({ emoji: "hammer", command: "Build" })

const result = await ctx.processModules({
const results = await ctx.processModules({
modules,
watch: opts.watch,
process: async (module) => {
return [await BuildTask.factory({ ctx, module, force: opts.force })]
},
})

ctx.log.info("")
ctx.log.header({ emoji: "heavy_check_mark", command: `Done!` })

return result
return handleTaskResults(ctx, "build", results)
}
}
10 changes: 6 additions & 4 deletions src/commands/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,12 @@ export class CallCommand extends Command<typeof callArgs> {
res.data && ctx.log.info(chalk.white(resStr))

return {
serviceName,
path,
url,
response: pick(res, ["status", "statusText", "headers", "data"]),
result: {
serviceName,
path,
url,
response: pick(res, ["status", "statusText", "headers", "data"]),
},
}
}
}
16 changes: 11 additions & 5 deletions src/commands/config/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
*/

import { PluginContext } from "../../plugin-context"
import { Command, ParameterValues, StringParameter } from "../base"
import { DeleteConfigResult } from "../../types/plugin/outputs"
import {
Command,
CommandResult,
ParameterValues,
StringParameter,
} from "../base"
import { NotFoundError } from "../../exceptions"

export const configDeleteArgs = {
Expand All @@ -28,16 +34,16 @@ export class ConfigDeleteCommand extends Command<typeof configDeleteArgs> {

arguments = configDeleteArgs

async action(ctx: PluginContext, args: DeleteArgs) {
async action(ctx: PluginContext, args: DeleteArgs): Promise<CommandResult<DeleteConfigResult>> {
const key = args.key.split(".")
const res = await ctx.deleteConfig({ key })
const result = await ctx.deleteConfig({ key })

if (res.found) {
if (result.found) {
ctx.log.info(`Deleted config key ${args.key}`)
} else {
throw new NotFoundError(`Could not find config key ${args.key}`, { key })
}

return { ok: true }
return { result }
}
}
2 changes: 1 addition & 1 deletion src/commands/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ export class ConfigCommand extends Command {
ConfigDeleteCommand,
]

async action() { }
async action() { return {} }
}
14 changes: 10 additions & 4 deletions src/commands/config/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
*/

import { PluginContext } from "../../plugin-context"
import { Command, ParameterValues, StringParameter } from "../base"
import { SetConfigResult } from "../../types/plugin/outputs"
import {
Command,
CommandResult,
ParameterValues,
StringParameter,
} from "../base"

export const configSetArgs = {
key: new StringParameter({
Expand All @@ -30,10 +36,10 @@ export class ConfigSetCommand extends Command<typeof configSetArgs> {

arguments = configSetArgs

async action(ctx: PluginContext, args: SetArgs) {
async action(ctx: PluginContext, args: SetArgs): Promise<CommandResult<SetConfigResult>> {
const key = args.key.split(".")
await ctx.setConfig({ key, value: args.value })
const result = await ctx.setConfig({ key, value: args.value })
ctx.log.info(`Set config key ${args.key}`)
return { ok: true }
return { result }
}
}
20 changes: 12 additions & 8 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@

import { PluginContext } from "../plugin-context"
import { DeployTask } from "../tasks/deploy"
import { BooleanParameter, Command, ParameterValues, StringParameter } from "./base"
import {
BooleanParameter,
Command,
CommandResult,
handleTaskResults,
ParameterValues,
StringParameter,
} from "./base"
import { TaskResults } from "../task-graph"

export const deployArgs = {
Expand All @@ -34,13 +41,13 @@ export class DeployCommand extends Command<typeof deployArgs, typeof deployOpts>
arguments = deployArgs
options = deployOpts

async action(ctx: PluginContext, args: Args, opts: Opts): Promise<TaskResults> {
async action(ctx: PluginContext, args: Args, opts: Opts): Promise<CommandResult<TaskResults>> {
const names = args.service ? args.service.split(",") : undefined
const services = await ctx.getServices(names)

if (services.length === 0) {
ctx.log.warn({ msg: "No services found. Aborting." })
return {}
return { result: {} }
}

ctx.log.header({ emoji: "rocket", command: "Deploy" })
Expand All @@ -52,17 +59,14 @@ export class DeployCommand extends Command<typeof deployArgs, typeof deployOpts>
const force = opts.force
const forceBuild = opts["force-build"]

const result = await ctx.processServices({
const results = await ctx.processServices({
services,
watch,
process: async (service) => {
return [await DeployTask.factory({ ctx, service, force, forceBuild })]
},
})

ctx.log.info("")
ctx.log.header({ emoji: "heavy_check_mark", command: `Done!` })

return result
return handleTaskResults(ctx, "deploy", results)
}
}
4 changes: 3 additions & 1 deletion src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class DevCommand extends Command {
ctx.log.info({ msg: "No modules found in project." })
}
ctx.log.info({ msg: "Aborting..." })
return
return {}
}

await ctx.processModules({
Expand All @@ -61,6 +61,8 @@ export class DevCommand extends Command {
}
},
})

return {}
}
}

Expand Down
9 changes: 6 additions & 3 deletions src/commands/environment/destroy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,26 @@

import { PluginContext } from "../../plugin-context"

import { Command } from "../base"
import {
Command,
CommandResult,
} from "../base"
import { EnvironmentStatusMap } from "../../types/plugin/outputs"

export class EnvironmentDestroyCommand extends Command {
name = "destroy"
alias = "d"
help = "Destroy environment"

async action(ctx: PluginContext): Promise<EnvironmentStatusMap> {
async action(ctx: PluginContext): Promise<CommandResult<EnvironmentStatusMap>> {
const { name } = ctx.getEnvironment()
ctx.log.header({ emoji: "skull_and_crossbones", command: `Destroying ${name} environment` })

const result = await ctx.destroyEnvironment({})

ctx.log.finish()

return result
return { result }
}

}
2 changes: 1 addition & 1 deletion src/commands/environment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ export class EnvironmentCommand extends Command {
EnvironmentDestroyCommand,
]

async action() { }
async action() { return {} }
}
Loading

0 comments on commit 36ba7b7

Please sign in to comment.