feat: Allow Kong to exit with semantic exit codes (#507)

* feat: Allow Kong to exit with semantic exit codes

At Block, we've instrumented a number of commandline tools and set SLOs on some tools' reliability. To do that effectively, we had to partition usage errors from reliability issues. We looked at [prior art](https://github.com/square/exit?tab=readme-ov-file#reserved-codes-and-prior-art) and, taking inspiration from HTTP, defined [a set of semantic exit codes](https://github.com/square/exit?tab=readme-ov-file#about) in ranges: 80-99 for user errors, 100-119 for system errors.

We've been wrapping errors in `exit.Error` at whatever level of the stack can tell which class an error is and unwrapping them at exit (`os.Exit(exit.FromError(err))`).

This adds support for semantic exit codes to Kong, to `FatalIfErrorf`, which is used internally by `kong.Parse` and often used in Kong applications.

* feat: Exit 80 (Usage Error) when usage is syntactically or semantically invalid

* refactor: Always exit 80 (Usage Error) on a `ParseError` but don't wrap errors from hooks in `ParseError`
This commit is contained in:
Bob Lail
2025-03-10 23:21:09 -07:00
committed by GitHub
parent 73db2e86a5
commit a86adbbb25
4 changed files with 56 additions and 12 deletions
+12 -9
View File
@@ -311,35 +311,35 @@ func (k *Kong) extraFlags() []*Flag {
// invalid one, which will report a normal error).
func (k *Kong) Parse(args []string) (ctx *Context, err error) {
ctx, err = Trace(k, args)
if err != nil {
if err != nil { // Trace is not expected to return an err
return nil, err
}
if ctx.Error != nil {
return nil, &ParseError{error: ctx.Error, Context: ctx}
}
if err = k.applyHook(ctx, "BeforeReset"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
return nil, err
}
if err = ctx.Reset(); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
if err = k.applyHook(ctx, "BeforeResolve"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
return nil, err
}
if err = ctx.Resolve(); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
if err = k.applyHook(ctx, "BeforeApply"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
return nil, err
}
if _, err = ctx.Apply(); err != nil {
return nil, &ParseError{error: err, Context: ctx}
if _, err = ctx.Apply(); err != nil { // Apply is not expected to return an err
return nil, err
}
if err = ctx.Validate(); err != nil {
return nil, &ParseError{error: err, Context: ctx}
}
if err = k.applyHook(ctx, "AfterApply"); err != nil {
return nil, &ParseError{error: err, Context: ctx}
return nil, err
}
return ctx, nil
}
@@ -428,13 +428,15 @@ func (k *Kong) Errorf(format string, args ...any) *Kong {
return k
}
// Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with a non-zero status.
// Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with status 1.
func (k *Kong) Fatalf(format string, args ...any) {
k.Errorf(format, args...)
k.Exit(1)
}
// FatalIfErrorf terminates with an error message if err != nil.
// If the error implements the ExitCoder interface, the ExitCode() method is called and
// the application exits with that status. Otherwise, the application exits with status 1.
func (k *Kong) FatalIfErrorf(err error, args ...any) {
if err == nil {
return
@@ -455,7 +457,8 @@ func (k *Kong) FatalIfErrorf(err error, args ...any) {
fmt.Fprintln(k.Stdout)
}
}
k.Fatalf("%s", msg)
k.Errorf("%s", msg)
k.Exit(exitCodeFromError(err))
}
// LoadConfig from path using the loader configured via Configuration(loader).