Skip to content

feat : add consistent exit codes - #3130

Open
vikashkumar2020 wants to merge 4 commits into
tektoncd:mainfrom
vikashkumar2020:feature/consistent-exit-codes
Open

feat : add consistent exit codes#3130
vikashkumar2020 wants to merge 4 commits into
tektoncd:mainfrom
vikashkumar2020:feature/consistent-exit-codes

Conversation

@vikashkumar2020

@vikashkumar2020 vikashkumar2020 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Changes

Fixes : #2855
Implemented consistent exit codes across tkn. Added a new pkg/exitcode package defining codes 0–5 (success, general error, not found, invalid input, timeout, unauthorized) with a typed error that propagates through the call stack. All Kubernetes API calls in pkg/actions now classify errors at the boundary via exitcode.FromAPIError(). The root command wires up exitcode.CodeFrom() to drive os.Exit, and adds structured {"error":"…","code":N} JSON output to stderr when --output json is set. A reference doc at docs/exit-codes.md covers the full scheme with examples.

Submitter Checklist

These are the criteria that every PR should meet, please check them off as you
review them:

  • Includes tests (if functionality changed/added)
  • Run the code checkers with make check
  • Regenerate the manpages, docs and go formatting with make generated
  • Commit messages follow commit message best practices

See the contribution guide
for more details.

Release Notes

The consistent exit codes with typed errors, k8s API error classification, and structured JSON error output for --output json is added

@tekton-robot tekton-robot added the release-note Denotes a PR that will be considered when it comes time to generate release notes. label Aug 11, 2026
@tekton-robot

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
To complete the pull request process, please assign divyansh42 after the PR has been reviewed.
You can assign the PR to them by writing /assign @divyansh42 in a comment when ready.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@tekton-robot tekton-robot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Aug 11, 2026
@divyansh42

Copy link
Copy Markdown
Member

@vikashkumar2020 thanks for the PR!
Could you please add release notes and link it to the corresponding GitHub issue?

@vikashkumar2020

Copy link
Copy Markdown
Contributor Author

@vikashkumar2020 thanks for the PR! Could you please add release notes and link it to the corresponding GitHub issue?

added into description

@divyansh42 divyansh42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR wraps errors in pkg/actions (get, list, delete, patch) but many commands produce errors through other paths: direct client calls in trigger/bundle commands, flag validation errors in RunE, etc. The issue asks for consistent exit codes "across all commands."

This is acceptable as a first pass, but please file a follow-up issue tracking the remaining commands so this doesn't fall through the cracks.

Comment thread pkg/exitcode/exitcode.go
if err == nil {
return nil
}
switch {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The InvalidInput constant is declared but nothing in this PR ever emits exit code 3. FromAPIError doesn't map k8serrors.IsInvalid() or k8serrors.IsBadRequest() to it, and Cobra flag-parsing errors also bypass this classification entirely.

We can probably do

case k8serrors.IsInvalid(err), k8serrors.IsBadRequest(err):
    return &Error{Code: InvalidInput, Message: err.Error()}

Comment thread pkg/exitcode/exitcode.go
Message string
}

func (e *Error) Error() string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FromAPIError creates a new *exitcode.Error with only the string message, discarding the original Kubernetes *StatusError. Any downstream code that calls k8serrors.IsNotFound(err) or errors.As(err, &statusErr) after a pkg/actions call will now get false because the original error is no longer in the chain.

Can we store the original and implement Unwrap():

type Error struct {
    Code    int
    Message string
    Err     error
}
func (e *Error) Error() string { return e.Message }
func (e *Error) Unwrap() error { return e.Err }

And update FromAPIError to preserve the original:

return &Error{Code: NotFound, Message: err.Error(), Err: err}

This preserves both the exit code semantics and the original error chain

Comment thread pkg/cmd/root.go
// "json", the error is serialised as {"error":"<message>","code":<n>}.
// Otherwise the standard "Error: <message>\n" format is used.
func PrintError(cmd *cobra.Command, err error, errW *os.File) {
outputFlag, _ := cmd.Flags().GetString("output")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PrintError calls cmd.Flags().GetString("output") where cmd is the root tkn command (passed from cmd/tkn/main.go). However, the --output flag is defined on individual subcommands, not on the root. GetString("output") on the root will return "" (and a non-nil error which is silently discarded), so the JSON error path will never trigger.

Consider resolving the executed subcommand's flags instead, or propagating the output format via a persistent pre-run hook that stores it somewhere accessible at error-handling time.

Comment thread pkg/exitcode/exitcode.go
return &Error{Code: NotFound, Message: err.Error()}
case k8serrors.IsUnauthorized(err), k8serrors.IsForbidden(err):
return &Error{Code: Unauthorized, Message: err.Error()}
case k8serrors.IsTimeout(err):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

k8serrors.IsTimeout() only matches HTTP 504 from the API server. Context deadline exceeded errors (context.DeadlineExceeded) and client-side request timeouts won't produce exit code 4.

Can we add this before the default case

case errors.Is(err, context.DeadlineExceeded):
    return &Error{Code: Timeout, Message: err.Error(), Err: err}

// PipelineRun has no conditions yet; treat as a general failure
// so the caller can distinguish from a successful run (0) without
// conflicting with the "resource not found" exit code (2).
return 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the "no conditions yet" exit code from 2 to 1 is semantically correct under the new scheme, but it's a breaking change for scripts relying on the previous behavior. The release note should explicitly mention this: "The exit code for PipelineRuns with no conditions has changed from 2 to 1."

Comment thread docs/exit-codes.md

| Code | Constant | Meaning |
|------|----------------|--------------------------------------------|
| `0` | `Success` | The command completed successfully. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Bash, exit code 2 conventionally means "incorrect usage / invalid arguments." This PR uses 2 for "not found" instead. This isn't necessarily wrong (kubectl doesn't differentiate at all, and many CLIs define their own schemes), but it's worth documenting this divergence explicitly in docs/exit-codes.md so users don't confuse it with the shell convention.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements a shared exit-code scheme for tkn by introducing a typed error that carries an intended exit code, classifying certain Kubernetes API failures at the pkg/actions boundary, and wiring the top-level CLI entrypoint to exit using the resolved code (with documentation for consumers).

Changes:

  • Added pkg/exitcode with standard codes (0–5), a typed exitcode.Error, and Kubernetes API error classification helpers.
  • Updated the top-level command execution path to silence Cobra’s default error printing and to drive os.Exit(...) from exitcode.CodeFrom(err), plus structured JSON error output support.
  • Updated selected actions helpers to wrap API errors via exitcode.FromAPIError(...) and aligned pipelinerun logs --exit-with-error behavior with the new scheme.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pkg/exitcode/exitcode.go Defines exit codes, typed error, and K8s API error classification.
pkg/exitcode/exitcode_test.go Unit tests for CodeFrom and FromAPIError.
pkg/cmd/root.go Silences Cobra errors and adds PrintError to emit JSON error payloads for --output json.
cmd/tkn/main.go Switches to centralized error printing + exit code resolution.
pkg/actions/get.go Wraps dynamic client Get errors via exitcode.FromAPIError.
pkg/actions/list.go Wraps dynamic client List errors via exitcode.FromAPIError.
pkg/actions/delete.go Wraps dynamic client Delete errors via exitcode.FromAPIError.
pkg/actions/patch.go Wraps dynamic client Patch errors via exitcode.FromAPIError.
pkg/cmd/pipelinerun/logs.go Adjusts “no conditions” exit code to avoid conflicting with NotFound.
pkg/cmd/pipelinerun/logs_test.go Updates the corresponding unit test expectation.
docs/exit-codes.md Documents the exit code table, examples, and structured JSON stderr errors.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmd/tkn/main.go
Comment on lines 51 to 55
CoreTkn:
if err := tkn.Execute(); err != nil {
os.Exit(1)
tknCmd.PrintError(tkn, err, os.Stderr)
os.Exit(exitcode.CodeFrom(err))
}
Comment thread pkg/actions/patch.go
Comment on lines 36 to +39
unstructuredObj, err := clients.Dynamic.Resource(*gvr).Namespace(ns).Patch(context.Background(), objName, types.JSONPatchType, data, opt)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to patch object from %s namespace \n", ns)
return err
return exitcode.FromAPIError(err)
Comment thread pkg/exitcode/exitcode.go
Comment on lines +73 to +82
switch {
case k8serrors.IsNotFound(err):
return &Error{Code: NotFound, Message: err.Error()}
case k8serrors.IsUnauthorized(err), k8serrors.IsForbidden(err):
return &Error{Code: Unauthorized, Message: err.Error()}
case k8serrors.IsTimeout(err):
return &Error{Code: Timeout, Message: err.Error()}
default:
return err
}
Comment thread pkg/actions/list.go
Comment on lines 59 to 62
allRes, err := dynamic.Resource(*gvr).Namespace(ns).List(context.Background(), op)
if err != nil {
return nil, err
return nil, exitcode.FromAPIError(err)
}
Comment thread pkg/cmd/root.go
Comment on lines +128 to +132
// PrintError writes err to errW. When the resolved --output flag on cmd is
// "json", the error is serialised as {"error":"<message>","code":<n>}.
// Otherwise the standard "Error: <message>\n" format is used.
func PrintError(cmd *cobra.Command, err error, errW *os.File) {
outputFlag, _ := cmd.Flags().GetString("output")
@divyansh42

Copy link
Copy Markdown
Member

One more minor comment:
The new files have Copyright © 2024 but this PR is authored in 2026. Please update to 2026.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Suppressed comments (2)

cmd/tkn/main.go:53

  • PrintError is given the root command, but --output is a local flag on the executed leaf command (PrintFlags.AddFlags uses cmd.Flags()). The lookup therefore fails and is ignored, so even tkn pipelinerun describe missing --output json emits the plain-text error instead of JSON. Use ExecuteC and pass its returned command; an integration test with a leaf --output json flag would catch this.
	if err := tkn.Execute(); err != nil {
		tknCmd.PrintError(tkn, err, os.Stderr)

pkg/actions/list.go:61

  • The API-boundary audit is incomplete. pkg/actions/create.go:34 and pkg/actions/watch.go:35 still return raw Kubernetes API errors, so commands using actions.Create or actions.Watch return code 1 for forbidden or timeout responses instead of codes 5 or 4. Apply the same classification to every API operation in this package.
	allRes, err := dynamic.Resource(*gvr).Namespace(ns).List(context.Background(), op)
	if err != nil {
		return nil, exitcode.FromAPIError(err)

Comment thread pkg/exitcode/exitcode.go
Comment on lines +78 to +79
case k8serrors.IsTimeout(err):
return &Error{Code: Timeout, Message: err.Error()}
Comment thread pkg/exitcode/exitcode.go
Comment on lines +43 to +44
// InvalidInput is the exit code for invalid flags, parameters, or input.
InvalidInput = 3
Comment thread pkg/exitcode/exitcode.go
Comment on lines +66 to +68
// FromAPIError converts a Kubernetes API error into an Error with the
// appropriate exit code. If err is not a k8s status error it is returned
// unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note Denotes a PR that will be considered when it comes time to generate release notes. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Define consistent exit codes for all tkn commands

4 participants