| Index: milo/appengine/job_source/buildbot/build.go
|
| diff --git a/milo/appengine/job_source/buildbot/build.go b/milo/appengine/job_source/buildbot/build.go
|
| deleted file mode 100644
|
| index 54114572ca70699e6a686ade491f72cd3945301d..0000000000000000000000000000000000000000
|
| --- a/milo/appengine/job_source/buildbot/build.go
|
| +++ /dev/null
|
| @@ -1,661 +0,0 @@
|
| -// Copyright 2016 The LUCI Authors. All rights reserved.
|
| -// Use of this source code is governed under the Apache License, Version 2.0
|
| -// that can be found in the LICENSE file.
|
| -
|
| -package buildbot
|
| -
|
| -import (
|
| - "encoding/json"
|
| - "errors"
|
| - "fmt"
|
| - "io/ioutil"
|
| - "math"
|
| - "path/filepath"
|
| - "regexp"
|
| - "sort"
|
| - "strconv"
|
| - "strings"
|
| - "time"
|
| -
|
| - "golang.org/x/net/context"
|
| -
|
| - "github.com/luci/gae/service/datastore"
|
| - "github.com/luci/luci-go/common/data/stringset"
|
| - "github.com/luci/luci-go/common/logging"
|
| - "github.com/luci/luci-go/milo/api/resp"
|
| - "github.com/luci/luci-go/milo/appengine/common/model"
|
| -)
|
| -
|
| -var errBuildNotFound = errors.New("Build not found")
|
| -
|
| -// getBuild fetches a buildbot build from the datastore and checks ACLs.
|
| -// The return code matches the master responses.
|
| -func getBuild(c context.Context, master, builder string, buildNum int) (*buildbotBuild, error) {
|
| - result := &buildbotBuild{
|
| - Master: master,
|
| - Buildername: builder,
|
| - Number: buildNum,
|
| - }
|
| -
|
| - err := datastore.Get(c, result)
|
| - err = checkAccess(c, err, result.Internal)
|
| - if err == errMasterNotFound {
|
| - err = errBuildNotFound
|
| - }
|
| -
|
| - return result, err
|
| -}
|
| -
|
| -// result2Status translates a buildbot result integer into a model.Status.
|
| -func result2Status(s *int) (status model.Status) {
|
| - if s == nil {
|
| - return model.Running
|
| - }
|
| - switch *s {
|
| - case 0:
|
| - status = model.Success
|
| - case 1:
|
| - status = model.Warning
|
| - case 2:
|
| - status = model.Failure
|
| - case 3:
|
| - status = model.NotRun // Skipped
|
| - case 4:
|
| - status = model.Exception
|
| - case 5:
|
| - status = model.WaitingDependency // Retry
|
| - default:
|
| - panic(fmt.Errorf("Unknown status %d", s))
|
| - }
|
| - return
|
| -}
|
| -
|
| -// buildbotTimeToTime converts a buildbot time representation (pointer to float
|
| -// of seconds since epoch) to a native time.Time object.
|
| -func buildbotTimeToTime(t *float64) (result time.Time) {
|
| - if t != nil {
|
| - result = time.Unix(int64(*t), int64(*t*1e9)%1e9).UTC()
|
| - }
|
| - return
|
| -}
|
| -
|
| -// parseTimes translates a buildbot time tuple (start, end) into a triplet
|
| -// of (Started time, Ending time, duration).
|
| -// If times[1] is nil and buildFinished is not, ended will be set to buildFinished
|
| -// time.
|
| -func parseTimes(buildFinished *float64, times []*float64) (started, ended time.Time, duration time.Duration) {
|
| - if len(times) != 2 {
|
| - panic(fmt.Errorf("Expected 2 floats for times, got %v", times))
|
| - }
|
| - if times[0] == nil {
|
| - // Some steps don't have timing info. In that case, just return nils.
|
| - return
|
| - }
|
| - started = buildbotTimeToTime(times[0])
|
| - switch {
|
| - case times[1] != nil:
|
| - ended = buildbotTimeToTime(times[1])
|
| - duration = ended.Sub(started)
|
| - case buildFinished != nil:
|
| - ended = buildbotTimeToTime(buildFinished)
|
| - duration = ended.Sub(started)
|
| - default:
|
| - duration = time.Since(started)
|
| - }
|
| - return
|
| -}
|
| -
|
| -// getBanner parses the OS information from the build and maybe returns a banner.
|
| -func getBanner(c context.Context, b *buildbotBuild) *resp.LogoBanner {
|
| - logging.Infof(c, "OS: %s/%s", b.OSFamily, b.OSVersion)
|
| - osLogo := func() *resp.Logo {
|
| - result := &resp.Logo{}
|
| - switch b.OSFamily {
|
| - case "windows":
|
| - result.LogoBase = resp.Windows
|
| - case "Darwin":
|
| - result.LogoBase = resp.OSX
|
| - case "Debian":
|
| - result.LogoBase = resp.Ubuntu
|
| - default:
|
| - return nil
|
| - }
|
| - result.Subtitle = b.OSVersion
|
| - return result
|
| - }()
|
| - if osLogo != nil {
|
| - return &resp.LogoBanner{
|
| - OS: []resp.Logo{*osLogo},
|
| - }
|
| - }
|
| - logging.Warningf(c, "No OS info found.")
|
| - return nil
|
| -}
|
| -
|
| -// summary extracts the top level summary from a buildbot build as a
|
| -// BuildComponent
|
| -func summary(c context.Context, b *buildbotBuild) resp.BuildComponent {
|
| - // TODO(hinoka): use b.toStatus()
|
| - // Status
|
| - var status model.Status
|
| - if b.Currentstep != nil {
|
| - status = model.Running
|
| - } else {
|
| - status = result2Status(b.Results)
|
| - }
|
| -
|
| - // Timing info
|
| - started, ended, duration := parseTimes(nil, b.Times)
|
| -
|
| - // Link to bot and original build.
|
| - host := "build.chromium.org/p"
|
| - if b.Internal {
|
| - host = "uberchromegw.corp.google.com/i"
|
| - }
|
| - bot := resp.NewLink(
|
| - b.Slave,
|
| - fmt.Sprintf("https://%s/%s/buildslaves/%s", host, b.Master, b.Slave),
|
| - )
|
| - source := resp.NewLink(
|
| - fmt.Sprintf("%s/%s/%d", b.Master, b.Buildername, b.Number),
|
| - fmt.Sprintf("https://%s/%s/builders/%s/builds/%d",
|
| - host, b.Master, b.Buildername, b.Number),
|
| - )
|
| -
|
| - // The link to the builder page.
|
| - parent := resp.NewLink(b.Buildername, ".")
|
| -
|
| - // Do a best effort lookup for the bot information to fill in OS/Platform info.
|
| - banner := getBanner(c, b)
|
| -
|
| - sum := resp.BuildComponent{
|
| - ParentLabel: parent,
|
| - Label: fmt.Sprintf("#%d", b.Number),
|
| - Banner: banner,
|
| - Status: status,
|
| - Started: started,
|
| - Finished: ended,
|
| - Bot: bot,
|
| - Source: source,
|
| - Duration: duration,
|
| - Type: resp.Summary, // This is more or less ignored.
|
| - LevelsDeep: 1,
|
| - Text: []string{}, // Status messages. Eg "This build failed on..xyz"
|
| - }
|
| -
|
| - return sum
|
| -}
|
| -
|
| -var rLineBreak = regexp.MustCompile("<br */?>")
|
| -
|
| -// components takes a full buildbot build struct and extract step info from all
|
| -// of the steps and returns it as a list of milo Build Components.
|
| -func components(b *buildbotBuild) (result []*resp.BuildComponent) {
|
| - endingTime := b.Times[1]
|
| - for _, step := range b.Steps {
|
| - if step.Hidden == true {
|
| - continue
|
| - }
|
| - bc := &resp.BuildComponent{
|
| - Label: step.Name,
|
| - }
|
| - // Step text sometimes contains <br>, which we want to parse into new lines.
|
| - for _, t := range step.Text {
|
| - for _, line := range rLineBreak.Split(t, -1) {
|
| - bc.Text = append(bc.Text, line)
|
| - }
|
| - }
|
| -
|
| - // Figure out the status.
|
| - if !step.IsStarted {
|
| - bc.Status = model.NotRun
|
| - } else if !step.IsFinished {
|
| - bc.Status = model.Running
|
| - } else {
|
| - if len(step.Results) > 0 {
|
| - status := int(step.Results[0].(float64))
|
| - bc.Status = result2Status(&status)
|
| - } else {
|
| - bc.Status = model.Success
|
| - }
|
| - }
|
| -
|
| - // Raise the interesting-ness if the step is not "Success".
|
| - if bc.Status != model.Success {
|
| - bc.Verbosity = resp.Interesting
|
| - }
|
| -
|
| - remainingAliases := stringset.New(len(step.Aliases))
|
| - for linkAnchor := range step.Aliases {
|
| - remainingAliases.Add(linkAnchor)
|
| - }
|
| -
|
| - getLinksWithAliases := func(logLink *resp.Link, isLog bool) resp.LinkSet {
|
| - // Generate alias links.
|
| - var aliases resp.LinkSet
|
| - if remainingAliases.Del(logLink.Label) {
|
| - stepAliases := step.Aliases[logLink.Label]
|
| - aliases = make(resp.LinkSet, len(stepAliases))
|
| - for i, alias := range stepAliases {
|
| - aliases[i] = alias.toLink()
|
| - }
|
| - }
|
| -
|
| - // Step log link takes primary, with aliases as secondary.
|
| - links := make(resp.LinkSet, 1, 1+len(aliases))
|
| - links[0] = logLink
|
| -
|
| - for _, a := range aliases {
|
| - a.Alias = true
|
| - }
|
| - return append(links, aliases...)
|
| - }
|
| -
|
| - for _, l := range step.Logs {
|
| - logLink := resp.NewLink(l[0], l[1])
|
| -
|
| - links := getLinksWithAliases(logLink, true)
|
| - if logLink.Label == "stdio" {
|
| - bc.MainLink = links
|
| - } else {
|
| - bc.SubLink = append(bc.SubLink, links)
|
| - }
|
| - }
|
| -
|
| - // Step links are stored as maps of name: url
|
| - // Because Go doesn't believe in nice things, we now create another array
|
| - // just so that we can iterate through this map in order.
|
| - names := make([]string, 0, len(step.Urls))
|
| - for name := range step.Urls {
|
| - names = append(names, name)
|
| - }
|
| - sort.Strings(names)
|
| - for _, name := range names {
|
| - logLink := resp.NewLink(name, step.Urls[name])
|
| -
|
| - bc.SubLink = append(bc.SubLink, getLinksWithAliases(logLink, false))
|
| - }
|
| -
|
| - // Add any unused aliases directly.
|
| - if remainingAliases.Len() > 0 {
|
| - unusedAliases := remainingAliases.ToSlice()
|
| - sort.Strings(unusedAliases)
|
| -
|
| - for _, label := range unusedAliases {
|
| - var baseLink resp.LinkSet
|
| - for _, alias := range step.Aliases[label] {
|
| - aliasLink := alias.toLink()
|
| - if len(baseLink) == 0 {
|
| - aliasLink.Label = label
|
| - } else {
|
| - aliasLink.Alias = true
|
| - }
|
| - baseLink = append(baseLink, aliasLink)
|
| - }
|
| -
|
| - if len(baseLink) > 0 {
|
| - bc.SubLink = append(bc.SubLink, baseLink)
|
| - }
|
| - }
|
| - }
|
| -
|
| - // Figure out the times.
|
| - bc.Started, bc.Finished, bc.Duration = parseTimes(endingTime, step.Times)
|
| -
|
| - result = append(result, bc)
|
| - }
|
| - return
|
| -}
|
| -
|
| -// parseProp returns a string representation of v.
|
| -func parseProp(v interface{}) string {
|
| - // if v is a whole number, force it into an int. json.Marshal() would turn
|
| - // it into what looks like a float instead. We want this to remain and
|
| - // int instead of a number.
|
| - if vf, ok := v.(float64); ok {
|
| - if math.Floor(vf) == vf {
|
| - return fmt.Sprintf("%d", int64(vf))
|
| - }
|
| - }
|
| - // return the json representation of the value.
|
| - b, err := json.Marshal(v)
|
| - if err == nil {
|
| - return string(b)
|
| - }
|
| - return fmt.Sprintf("%v", v)
|
| -}
|
| -
|
| -// Prop is a struct used to store a value and group so that we can make a map
|
| -// of key:Prop to pass into parseProp() for the purpose of cross referencing
|
| -// one prop while working on another.
|
| -type Prop struct {
|
| - Value interface{}
|
| - Group string
|
| -}
|
| -
|
| -// properties extracts all properties from buildbot builds and groups them into
|
| -// property groups.
|
| -func properties(b *buildbotBuild) (result []*resp.PropertyGroup) {
|
| - groups := map[string]*resp.PropertyGroup{}
|
| - allProps := map[string]Prop{}
|
| - for _, prop := range b.Properties {
|
| - allProps[prop.Name] = Prop{
|
| - Value: prop.Value,
|
| - Group: prop.Source,
|
| - }
|
| - }
|
| - for key, prop := range allProps {
|
| - value := prop.Value
|
| - groupName := prop.Group
|
| - if _, ok := groups[groupName]; !ok {
|
| - groups[groupName] = &resp.PropertyGroup{GroupName: groupName}
|
| - }
|
| - vs := parseProp(value)
|
| - groups[groupName].Property = append(groups[groupName].Property, &resp.Property{
|
| - Key: key,
|
| - Value: vs,
|
| - })
|
| - }
|
| - // Insert the groups into a list in alphabetical order.
|
| - // You have to make a separate sorting data structure because Go doesn't like
|
| - // sorting things for you.
|
| - groupNames := []string{}
|
| - for n := range groups {
|
| - groupNames = append(groupNames, n)
|
| - }
|
| - sort.Strings(groupNames)
|
| - for _, k := range groupNames {
|
| - group := groups[k]
|
| - // Also take this oppertunity to sort the properties within the groups.
|
| - sort.Sort(group)
|
| - result = append(result, group)
|
| - }
|
| - return
|
| -}
|
| -
|
| -// blame extracts the commit and blame information from a buildbot build and
|
| -// returns it as a list of Commits.
|
| -func blame(b *buildbotBuild) (result []*resp.Commit) {
|
| - for _, c := range b.Sourcestamp.Changes {
|
| - files := c.GetFiles()
|
| - result = append(result, &resp.Commit{
|
| - AuthorEmail: c.Who,
|
| - Repo: c.Repository,
|
| - CommitTime: time.Unix(int64(c.When), 0).UTC(),
|
| - Revision: resp.NewLink(c.Revision, c.Revlink),
|
| - Description: c.Comments,
|
| - Title: strings.Split(c.Comments, "\n")[0],
|
| - File: files,
|
| - })
|
| - }
|
| - return
|
| -}
|
| -
|
| -// sourcestamp extracts the source stamp from various parts of a buildbot build,
|
| -// including the properties.
|
| -func sourcestamp(c context.Context, b *buildbotBuild) *resp.SourceStamp {
|
| - ss := &resp.SourceStamp{}
|
| - rietveld := ""
|
| - gerrit := ""
|
| - got_revision := ""
|
| - repository := ""
|
| - issue := int64(-1)
|
| - for _, prop := range b.Properties {
|
| - switch prop.Name {
|
| - case "rietveld":
|
| - if v, ok := prop.Value.(string); ok {
|
| - rietveld = v
|
| - } else {
|
| - logging.Warningf(c, "Field rietveld is not a string: %#v", prop.Value)
|
| - }
|
| - case "issue":
|
| - // Sometime this is a number (float), sometime it is a string.
|
| - if v, ok := prop.Value.(float64); ok {
|
| - issue = int64(v)
|
| - } else if v, ok := prop.Value.(string); ok {
|
| - if vi, err := strconv.ParseInt(v, 10, 64); err == nil {
|
| - issue = int64(vi)
|
| - } else {
|
| - logging.Warningf(c, "Could not decode field issue: %q - %s", prop.Value, err)
|
| - }
|
| - } else {
|
| - logging.Warningf(c, "Field issue is not a string or float: %#v", prop.Value)
|
| - }
|
| -
|
| - case "got_revision":
|
| - if v, ok := prop.Value.(string); ok {
|
| - got_revision = v
|
| - } else {
|
| - logging.Warningf(c, "Field got_revision is not a string: %#v", prop.Value)
|
| - }
|
| -
|
| - case "patch_issue":
|
| - if v, ok := prop.Value.(float64); ok {
|
| - issue = int64(v)
|
| - } else {
|
| - logging.Warningf(c, "Field patch_issue is not a float: %#v", prop.Value)
|
| - }
|
| -
|
| - case "patch_gerrit_url":
|
| - if v, ok := prop.Value.(string); ok {
|
| - gerrit = v
|
| - } else {
|
| - logging.Warningf(c, "Field gerrit is not a string: %#v", prop.Value)
|
| - }
|
| -
|
| - case "repository":
|
| - if v, ok := prop.Value.(string); ok {
|
| - repository = v
|
| - }
|
| - }
|
| - }
|
| - if issue != -1 {
|
| - switch {
|
| - case rietveld != "":
|
| - rietveld = strings.TrimRight(rietveld, "/")
|
| - ss.Changelist = resp.NewLink(
|
| - fmt.Sprintf("Rietveld CL %d", issue),
|
| - fmt.Sprintf("%s/%d", rietveld, issue))
|
| - case gerrit != "":
|
| - gerrit = strings.TrimRight(gerrit, "/")
|
| - ss.Changelist = resp.NewLink(
|
| - fmt.Sprintf("Gerrit CL %d", issue),
|
| - fmt.Sprintf("%s/c/%d", gerrit, issue))
|
| - }
|
| - }
|
| -
|
| - if got_revision != "" {
|
| - ss.Revision = resp.NewLink(got_revision, "")
|
| - if repository != "" {
|
| - ss.Revision.URL = repository + "/+/" + got_revision
|
| - }
|
| - }
|
| - return ss
|
| -}
|
| -
|
| -func renderBuild(c context.Context, b *buildbotBuild) *resp.MiloBuild {
|
| - // Modify the build for rendering.
|
| - updatePostProcessBuild(b)
|
| -
|
| - // TODO(hinoka): Do all fields concurrently.
|
| - return &resp.MiloBuild{
|
| - SourceStamp: sourcestamp(c, b),
|
| - Summary: summary(c, b),
|
| - Components: components(b),
|
| - PropertyGroup: properties(b),
|
| - Blame: blame(b),
|
| - }
|
| -}
|
| -
|
| -// DebugBuild fetches a debugging build for testing.
|
| -func DebugBuild(c context.Context, relBuildbotDir string, builder string, buildNum int) (*resp.MiloBuild, error) {
|
| - fname := fmt.Sprintf("%s.%d.json", builder, buildNum)
|
| - // ../buildbot below assumes that
|
| - // - this code is not executed by tests outside of this dir
|
| - // - this dir is a sibling of frontend dir
|
| - path := filepath.Join(relBuildbotDir, "testdata", fname)
|
| - raw, err := ioutil.ReadFile(path)
|
| - if err != nil {
|
| - return nil, err
|
| - }
|
| - b := &buildbotBuild{}
|
| - if err := json.Unmarshal(raw, b); err != nil {
|
| - return nil, err
|
| - }
|
| - return renderBuild(c, b), nil
|
| -}
|
| -
|
| -// Build fetches a buildbot build and translates it into a miloBuild.
|
| -func Build(c context.Context, master, builder string, buildNum int) (*resp.MiloBuild, error) {
|
| - b, err := getBuild(c, master, builder, buildNum)
|
| - if err != nil {
|
| - return nil, err
|
| - }
|
| - return renderBuild(c, b), nil
|
| -}
|
| -
|
| -// updatePostProcessBuild transforms a build from its raw JSON format into the
|
| -// format that should be presented to users.
|
| -//
|
| -// Post-processing includes:
|
| -// - If the build is LogDog-only, promotes aliases (LogDog links) to
|
| -// first-class links in the build.
|
| -func updatePostProcessBuild(b *buildbotBuild) {
|
| - // If this is a LogDog-only build, we want to promote the LogDog links.
|
| - if loc, ok := b.getPropertyValue("log_location").(string); ok && strings.HasPrefix(loc, "logdog://") {
|
| - linkMap := map[string]string{}
|
| - for sidx := range b.Steps {
|
| - promoteLogDogLinks(&b.Steps[sidx], sidx == 0, linkMap)
|
| - }
|
| -
|
| - // Update "Logs". This field is part of BuildBot, and is the amalgamation
|
| - // of all logs in the build's steps. Since each log is out of context of its
|
| - // original step, we can't apply the promotion logic; instead, we will use
|
| - // the link map to map any old URLs that were matched in "promoteLogDogLnks"
|
| - // to their new URLs.
|
| - for _, link := range b.Logs {
|
| - // "link" is in the form: [NAME, URL]
|
| - if len(link) != 2 {
|
| - continue
|
| - }
|
| -
|
| - if newURL, ok := linkMap[link[1]]; ok {
|
| - link[1] = newURL
|
| - }
|
| - }
|
| - }
|
| -}
|
| -
|
| -// promoteLogDogLinks updates the links in a BuildBot step to
|
| -// promote LogDog links.
|
| -//
|
| -// A build's links come in one of three forms:
|
| -// - Log Links, which link directly to BuildBot build logs.
|
| -// - URL Links, which are named links to arbitrary URLs.
|
| -// - Aliases, which attach to the label in one of the other types of links and
|
| -// augment it with additional named links.
|
| -//
|
| -// LogDog uses aliases exclusively to attach LogDog logs to other links. When
|
| -// the build is LogDog-only, though, the original links are actually junk. What
|
| -// we want to do is remove the original junk links and replace them with their
|
| -// alias counterparts, so that the "natural" BuildBot links are actually LogDog
|
| -// links.
|
| -//
|
| -// As URLs are re-mapped, the supplied "linkMap" will be updated to map the old
|
| -// URLs to the new ones.
|
| -func promoteLogDogLinks(s *buildbotStep, isInitialStep bool, linkMap map[string]string) {
|
| - type stepLog struct {
|
| - label string
|
| - url string
|
| - }
|
| -
|
| - remainingAliases := stringset.New(len(s.Aliases))
|
| - for linkAnchor := range s.Aliases {
|
| - remainingAliases.Add(linkAnchor)
|
| - }
|
| -
|
| - maybePromoteAliases := func(sl *stepLog, isLog bool) []*stepLog {
|
| - // As a special case, if this is the first step ("steps" in BuildBot), we
|
| - // will refrain from promoting aliases for "stdio", since "stdio" represents
|
| - // the raw BuildBot logs.
|
| - if isLog && isInitialStep && sl.label == "stdio" {
|
| - // No aliases, don't modify this log.
|
| - return []*stepLog{sl}
|
| - }
|
| -
|
| - // If there are no aliases, we should obviously not promote them. This will
|
| - // be the case for pre-LogDog steps such as build setup.
|
| - aliases := s.Aliases[sl.label]
|
| - if len(aliases) == 0 {
|
| - return []*stepLog{sl}
|
| - }
|
| -
|
| - // We have chosen to promote the aliases. Therefore, we will not include
|
| - // them as aliases in the modified step.
|
| - remainingAliases.Del(sl.label)
|
| -
|
| - result := make([]*stepLog, len(aliases))
|
| - for i, alias := range aliases {
|
| - aliasStepLog := stepLog{alias.Text, alias.URL}
|
| -
|
| - // Any link named "logdog" (Annotee cosmetic implementation detail) will
|
| - // inherit the name of the original log.
|
| - if isLog {
|
| - if aliasStepLog.label == "logdog" {
|
| - aliasStepLog.label = sl.label
|
| - }
|
| - }
|
| -
|
| - result[i] = &aliasStepLog
|
| - }
|
| -
|
| - // If we performed mapping, add the OLD -> NEW URL mapping to linkMap.
|
| - //
|
| - // Since multpiple aliases can apply to a single log, and we have to pick
|
| - // one, here, we'll arbitrarily pick the last one. This is maybe more
|
| - // consistent than the first one because linkMap, itself, will end up
|
| - // holding the last mapping for any given URL.
|
| - if len(result) > 0 {
|
| - linkMap[sl.url] = result[len(result)-1].url
|
| - }
|
| -
|
| - return result
|
| - }
|
| -
|
| - // Update step logs.
|
| - newLogs := make([][]string, 0, len(s.Logs))
|
| - for _, l := range s.Logs {
|
| - for _, res := range maybePromoteAliases(&stepLog{l[0], l[1]}, true) {
|
| - newLogs = append(newLogs, []string{res.label, res.url})
|
| - }
|
| - }
|
| - s.Logs = newLogs
|
| -
|
| - // Update step URLs.
|
| - newURLs := make(map[string]string, len(s.Urls))
|
| - for label, link := range s.Urls {
|
| - urlLinks := maybePromoteAliases(&stepLog{label, link}, false)
|
| - if len(urlLinks) > 0 {
|
| - // Use the last URL link, since our URL map can only tolerate one link.
|
| - // The expected case here is that len(urlLinks) == 1, though, but it's
|
| - // possible that multiple aliases can be included for a single URL, so
|
| - // we need to handle that.
|
| - newValue := urlLinks[len(urlLinks)-1]
|
| - newURLs[newValue.label] = newValue.url
|
| - } else {
|
| - newURLs[label] = link
|
| - }
|
| - }
|
| - s.Urls = newURLs
|
| -
|
| - // Preserve any aliases that haven't been promoted.
|
| - var newAliases map[string][]*buildbotLinkAlias
|
| - if l := remainingAliases.Len(); l > 0 {
|
| - newAliases = make(map[string][]*buildbotLinkAlias, l)
|
| - remainingAliases.Iter(func(v string) bool {
|
| - newAliases[v] = s.Aliases[v]
|
| - return true
|
| - })
|
| - }
|
| - s.Aliases = newAliases
|
| -}
|
|
|