bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
96 lines
3.6 KiB
Go
96 lines
3.6 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package output
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
)
|
|
|
|
// PartialFailureError is the exit signal for a batch / multi-status command that
|
|
// has already written an ok:false result envelope to stdout. The per-item
|
|
// outcomes are the primary, machine-readable output and live on stdout, so the
|
|
// dispatcher sets only the exit code and writes nothing to stderr.
|
|
//
|
|
// It is deliberately distinct from ErrBare (the stdout-carries-the-answer
|
|
// silent-exit signal) so that contract stays narrow, and from a typed *errs.XxxError
|
|
// (which owns the stderr error envelope): a partial failure is a result, not an
|
|
// error envelope.
|
|
type PartialFailureError struct {
|
|
Code int
|
|
}
|
|
|
|
func (e *PartialFailureError) Error() string {
|
|
return fmt.Sprintf("partial failure (exit %d)", e.Code)
|
|
}
|
|
|
|
// PartialFailure builds the partial-failure exit signal with the given code.
|
|
func PartialFailure(code int) *PartialFailureError {
|
|
return &PartialFailureError{Code: code}
|
|
}
|
|
|
|
// WriteTypedErrorEnvelope writes the JSON error envelope for a typed error.
|
|
// Each typed error owns its wire shape via its own struct tags: Problem fields
|
|
// are promoted to the top level through embedding, and extension fields
|
|
// (MissingScopes, ChallengeURL, etc.) sit alongside as siblings — not inside
|
|
// a `detail` sub-object.
|
|
//
|
|
// Two-stage write:
|
|
//
|
|
// 1. Serialize the envelope into an in-memory buffer. If serialization
|
|
// fails, return false so the dispatcher handles it via its signal /
|
|
// usage-error branches; nothing is written to w.
|
|
// 2. Best-effort write of the serialized bytes to w. A partial write is
|
|
// accepted (return value still true): the typed exit code has already
|
|
// been determined upstream by handleRootError calling ExitCodeOf(err)
|
|
// before this writer runs, so a torn envelope on stderr must not
|
|
// downgrade the caller's typed exit (3/4/6/10) to plain 1. Consumers
|
|
// parse-or-skip on malformed JSON.
|
|
//
|
|
// Returns true when err was a typed error and serialization succeeded.
|
|
// Returns false only when err carries no Problem (the dispatcher then handles
|
|
// it via its signal / usage-error branches) or when JSON encoding itself failed.
|
|
func WriteTypedErrorEnvelope(w io.Writer, err error, identity string) bool {
|
|
typed, ok := errs.UnwrapTypedError(err)
|
|
if !ok {
|
|
return false
|
|
}
|
|
env := typedEnvelope{
|
|
OK: false,
|
|
Identity: identity,
|
|
Error: typed,
|
|
Notice: GetNotice(),
|
|
}
|
|
var buf bytes.Buffer
|
|
enc := json.NewEncoder(&buf)
|
|
enc.SetEscapeHTML(false)
|
|
enc.SetIndent("", " ")
|
|
if encErr := enc.Encode(env); encErr != nil {
|
|
// Encoding failed — emit nothing here; the dispatcher's fall-through
|
|
// branches still surface the error, so stderr is never blank.
|
|
return false
|
|
}
|
|
// Best-effort write. Partial-write does not downgrade the success status:
|
|
// the dispatcher has already captured ExitCodeOf(err) before calling us,
|
|
// and a torn stderr is preferable to falling through to the plain
|
|
// "Error:" path with exit 1.
|
|
_, _ = w.Write(buf.Bytes())
|
|
return true
|
|
}
|
|
|
|
// typedEnvelope wraps a typed error for wire emission. Error is `error` so the
|
|
// underlying typed error's own json tags determine the inner shape via
|
|
// encoding/json reflection; Notice mirrors the success Envelope's notice (see
|
|
// GetNotice in envelope.go).
|
|
type typedEnvelope struct {
|
|
OK bool `json:"ok"`
|
|
Identity string `json:"identity,omitempty"`
|
|
Error error `json:"error"`
|
|
Notice map[string]interface{} `json:"_notice,omitempty"`
|
|
}
|