github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/deck/badge.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"bytes"
    21  	"html/template"
    22  
    23  	"path/filepath"
    24  	"sort"
    25  	"strings"
    26  
    27  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    28  )
    29  
    30  var svg = `<svg xmlns="http://www.w3.org/2000/svg" width="{{.Width}}" height="20">
    31  <linearGradient id="a" x2="0" y2="100%">
    32    <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
    33    <stop offset="1" stop-opacity=".1"/>
    34  </linearGradient>
    35  <rect rx="3" width="100%" height="20" fill="#555"/>
    36  <g fill="{{.Color}}">
    37    <rect rx="3" x="{{.RightStart}}" width="{{.RightWidth}}" height="20"/>
    38    <path d="M{{.RightStart}} 0h4v20h-4z"/>
    39  </g>
    40  <rect rx="3" width="100%" height="20" fill="url(#a)"/>
    41  <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
    42  <g fill="#010101" opacity=".3">
    43  <text x="{{.XposLeft}}" y="15">{{.Subject}}</text>
    44  <text x="{{.XposRight}}" y="15">{{.Status}}</text>
    45  </g>
    46  <text x="{{.XposLeft}}" y="14">{{.Subject}}</text>
    47  <text x="{{.XposRight}}" y="14">{{.Status}}</text>
    48  </g>
    49  </svg>`
    50  
    51  var svgTemplate = template.Must(template.New("svg").Parse(svg))
    52  
    53  // Make a small SVG badge that looks like `[subject | status]`, with the status
    54  // text in the given color. Provides a local version of the shields.io service.
    55  func makeShield(subject, status, color string) []byte {
    56  	// TODO(rmmh): Use better font-size metrics for prettier badges-- estimating
    57  	// character widths as 6px isn't very accurate.
    58  	// See also: https://github.com/badges/shields/blob/master/measure-text.js
    59  	p := struct {
    60  		Width, RightStart, RightWidth int
    61  		XposLeft, XposRight           float64
    62  		Subject, Status               string
    63  		Color                         string
    64  	}{
    65  		Subject:    subject,
    66  		Status:     status,
    67  		RightStart: 13 + 6*len(subject),
    68  		RightWidth: 13 + 6*len(status),
    69  	}
    70  	p.Width = p.RightStart + p.RightWidth
    71  	p.XposLeft = float64(p.RightStart) * 0.5
    72  	p.XposRight = float64(p.RightStart) + float64(p.RightWidth-2)*0.5
    73  	switch color {
    74  	case "brightgreen":
    75  		p.Color = "#4c1"
    76  	case "red":
    77  		p.Color = "#e05d44"
    78  	default:
    79  		p.Color = color
    80  	}
    81  	var buf bytes.Buffer
    82  	svgTemplate.Execute(&buf, p)
    83  	return buf.Bytes()
    84  }
    85  
    86  // pickLatestJobs returns the most recent run of each job matching the selector,
    87  // which is comma-separated list of globs, for example "ci-ti-*,ci-other".
    88  // jobs will be sorted by StartTime in reverse order to display recent jobs first
    89  func pickLatestJobs(jobs []prowapi.ProwJob, selector string) []prowapi.ProwJob {
    90  	sort.Slice(jobs, func(i, j int) bool {
    91  		return jobs[j].Status.StartTime.Before(&jobs[i].Status.StartTime)
    92  	})
    93  	var out []prowapi.ProwJob
    94  	have := make(map[string]bool)
    95  	want := strings.Split(selector, ",")
    96  	for _, job := range jobs {
    97  		if have[job.Spec.Job] {
    98  			continue // already have the latest result for this job
    99  		}
   100  		for _, pat := range want {
   101  			if match, _ := filepath.Match(pat, job.Spec.Job); match {
   102  				have[job.Spec.Job] = true
   103  				out = append(out, job)
   104  				break
   105  			}
   106  		}
   107  	}
   108  	return out
   109  }
   110  
   111  func renderBadge(jobs []prowapi.ProwJob) (string, string, []byte) {
   112  	color := "brightgreen"
   113  	status := "passing"
   114  	if len(jobs) == 0 {
   115  		color = "darkgrey"
   116  		status = "no results"
   117  	} else {
   118  		failedJobs := []string{}
   119  		for _, job := range jobs {
   120  			if job.Status.State == "failure" {
   121  				failedJobs = append(failedJobs, job.Spec.Job)
   122  			}
   123  		}
   124  		sort.Strings(failedJobs)
   125  		if len(failedJobs) > 3 {
   126  			failedJobs = append(failedJobs[:3], "...")
   127  		}
   128  		if len(failedJobs) > 0 {
   129  			color = "red"
   130  			status = "failing " + strings.Join(failedJobs, ", ")
   131  		}
   132  	}
   133  
   134  	return status, color, makeShield("build", status, color)
   135  }