blob: 05618703be1f375d8b7f8aac7e886b1e6a764623 [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package build
import (
"context"
"fmt"
"google.golang.org/protobuf/proto"
bbpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/buildbucket/protoutil"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
)
type buildStatus struct {
status bbpb.Status
details *bbpb.StatusDetails
}
var statusTag = errors.NewTagKey("build Status")
// AttachStatus attaches a buildbucket status (and details) to a given error.
//
// If such an error is handled by Step.End or State.End, that step/build will
// have its status updated to match.
//
// AttachStatus allows overriding the attached status if the error already has
// one.
//
// This panics if the status is a non-terminal status like SCHEDULED or STARTED.
//
// This is a no-op if the error is nil.
func AttachStatus(err error, status bbpb.Status, details *bbpb.StatusDetails) error {
if !protoutil.IsEnded(status) {
panic(errors.Reason("AttachStatus cannot be used with non-terminal status %q", status).Err())
}
if err == nil {
return nil
}
if details != nil {
details = proto.Clone(details).(*bbpb.StatusDetails)
}
return errors.TagValue{Key: statusTag, Value: &buildStatus{status, details}}.Apply(err)
}
// ExtractStatus retrieves the Buildbucket status (and details) from a given
// error.
//
// This returns:
// * (SUCCESS, nil) on nil error
// * Any values attached with AttachStatus.
// * (CANCELED, nil) on context.Canceled
// * (INFRA_FAILURE, &bbpb.StatusDetails{Timeout: {}}) on
// context.DeadlineExceeded
// * (FAILURE, nil) otherwise
//
// This function is used internally by Step.End and State.End, but is provided
// publically for completeness.
func ExtractStatus(err error) (bbpb.Status, *bbpb.StatusDetails) {
if err == nil {
return bbpb.Status_SUCCESS, nil
}
if value, ok := errors.TagValueIn(statusTag, err); ok {
bs := value.(*buildStatus)
details := bs.details
if details != nil {
details = proto.Clone(details).(*bbpb.StatusDetails)
}
return bs.status, details
}
switch err {
case context.Canceled:
return bbpb.Status_CANCELED, nil
case context.DeadlineExceeded:
return bbpb.Status_INFRA_FAILURE, &bbpb.StatusDetails{
Timeout: &bbpb.StatusDetails_Timeout{},
}
}
return bbpb.Status_FAILURE, nil
}
func computePanicStatus(err error) (status bbpb.Status, message string) {
if errors.IsPanicking(2) {
message = "PANIC"
// TODO(iannucci): include details of panic in SummaryMarkdown or log?
// How to prevent panic dump from showing up at every single step on the
// stack?
status = bbpb.Status_INFRA_FAILURE
} else {
status, _ = ExtractStatus(err)
if err != nil {
message = err.Error()
}
}
return
}
func logStatus(ctx context.Context, status bbpb.Status, message, markdown string) {
logf := logging.Errorf
switch status {
case bbpb.Status_SUCCESS:
logf = logging.Infof
case bbpb.Status_CANCELED:
logf = logging.Warningf
}
logMsg := fmt.Sprintf("set status: %s", status)
if len(message) > 0 {
logMsg += ": " + message
}
if len(markdown) > 0 {
logMsg += "\n with SummaryMarkdown:\n" + markdown
}
logf(ctx, "%s", logMsg)
}