blob: 9acc37d7331979935b8a71cef2f6699a450d6baa [file] [log] [blame]
// Copyright 2019 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 buildmerge
import (
"fmt"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
bbpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/buildbucket/protoutil"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/proto/reflectutil"
)
// setErrorOnBuild modifies `build` so that it's SummaryMarkdown contains `err`
// and its Status is INFRA_FAILURE.
func setErrorOnBuild(build *bbpb.Build, err error) {
addon := fmt.Sprintf("\n\nError in build protocol: %s", err)
// TODO(iannucci): extract this magical 4KB constant and trimming function
// into bbpb.
// TODO(iannucci): 4KB is pretty small.
if over := (len(build.SummaryMarkdown) + len(addon)) - 4093; over > 0 {
build.SummaryMarkdown = build.SummaryMarkdown[:len(build.SummaryMarkdown)-over] + "..."
}
build.SummaryMarkdown += addon
// TODO(crbug.com/1450399): Only update build.Output.Status
// after recipe_engine change is fully rolled out.
build.Status = bbpb.Status_INFRA_FAILURE
if build.Output == nil {
build.Output = &bbpb.Build_Output{}
}
build.Output.Status = bbpb.Status_INFRA_FAILURE
build.Output.SummaryMarkdown = build.SummaryMarkdown
}
// processFinalBuild adjusts the final state of the build if needed.
//
// This ensures that `build` has a final `Status` code (or marks the build as in
// an error'd state).
//
// Sets EndTime and UpdateTime for the build, and EndTime for all steps (if
// they're missing one). Any incomplete steps will be marked as "CANCELED".
//
// If this needs to make adjustments to `build.Steps`, it will make shallow
// copies of Steps, as well as the individual steps which need modification.
func processFinalBuild(now *timestamppb.Timestamp, build *bbpb.Build) {
if !protoutil.IsEnded(build.Output.GetStatus()) {
setErrorOnBuild(build, errors.Reason(
"Expected a terminal build status, got %s, while top level status is %s.",
build.Output.GetStatus(), build.Status).Err())
}
// Mark incomplete steps as canceled and set EndTime for all steps missing it.
newSteps := build.Steps
copiedSteps := false
for i, s := range build.Steps {
terminalStatus := protoutil.IsEnded(s.Status)
if terminalStatus && s.EndTime != nil {
continue
}
if s.GetMergeBuild().GetFromLogdogStream() != "" {
// It is okay merge step has a non-terminal status at this moment.
// The final merge will make sure this merge step is updated with
// the final state of sub build in the final build.
continue
}
// maybe make a copy of the Steps slice
if !copiedSteps {
copiedSteps = true
newSteps = make([]*bbpb.Step, len(build.Steps))
copy(newSteps, build.Steps)
}
s = reflectutil.ShallowCopy(s).(*bbpb.Step)
if !terminalStatus {
s.Status = bbpb.Status_CANCELED
if s.SummaryMarkdown != "" {
s.SummaryMarkdown += "\n"
}
s.SummaryMarkdown += "step was never finalized; did the build crash?"
}
if s.EndTime == nil {
s.EndTime = now
}
newSteps[i] = s
}
build.Steps = newSteps
build.UpdateTime = now
build.EndTime = now
}
// updateStepFromBuild adjusts `step` (presumably a "merge step") from the given
// build if the step status is not terminal; this overwrites the step's
// SummaryMarkdown, Status and EndTime to match that of `build`, and appends
// any Output.Logs from `build` to step.Log.
func updateStepFromBuild(step *bbpb.Step, build *bbpb.Build) {
if protoutil.IsEnded(step.Status) {
return
}
step.SummaryMarkdown = build.SummaryMarkdown
step.Status = build.Output.GetStatus()
// TODO(crbug.com/1450399): remove after recipe_engine change is fully rolled out.
if step.Status != build.Status {
step.Status = build.Status
}
step.EndTime = build.EndTime
for _, log := range build.Output.GetLogs() {
step.Logs = append(step.Logs, log)
}
}
// updateBaseFromUserBuild updates a 'template' base build with all the
// output-able data from `build`.
//
// In this scenario, `base` would be the input build to a task, and `build`
// would be the outputs from the running luciexe.
//
// As a special case, Output is proto.Merge'd.
//
// NOTE: This function assumes exclusive read/write access of `base` only
// (not its subfields), and assumes read access to subfields. This function must
// therefore only write to the immediate subfields of `base`; It may READ
// some fields deeply (such as `.Output`), but will not modify any subfields
// therein.
func updateBaseFromUserBuild(base, build *bbpb.Build) {
if build == nil {
return
}
base.SummaryMarkdown = build.SummaryMarkdown
base.Status = build.Status
base.StatusDetails = build.StatusDetails
base.UpdateTime = build.UpdateTime
base.EndTime = build.EndTime
base.Tags = build.Tags
if build.Output != nil {
var output *bbpb.Build_Output
if base.Output != nil {
output = proto.Clone(base.Output).(*bbpb.Build_Output)
} else {
output = &bbpb.Build_Output{}
}
proto.Merge(output, build.GetOutput())
base.Output = output
}
}
// Implements MergeBuild.legacy_global_namespace.
//
// Only used for legacy CrOS builders, see crbug.com/1310155.
//
// Merges:
// - properties will be 'merged' by replacing top-level keys. If the
// parent build and this sub-build both write in the same top-level keys
// the sub-build value wins.
//
// The following fields COULD be merged, but aren't, because this
// functionality is legacy, and we're not trying to make it do
// anything more than is necessary:
// - gitiles_commit will NOT be merged
// - the sub-build's output.logs will NOT be merged.
// - the sub-build's summary_markdown will NOT be merged.
func updateBuildFromGlobalSubBuild(parent, subBuild *bbpb.Build) {
subOut := subBuild.GetOutput()
if subP := subOut.GetProperties(); subP != nil {
if parent.Output == nil {
parent.Output = &bbpb.Build_Output{Properties: subP}
} else if parent.Output.Properties == nil {
parent.Output.Properties = subP
} else {
for key, item := range subP.GetFields() {
parent.Output.Properties.Fields[key] = item
}
}
}
}