internal/httpd/badge.go
89 lines · 2635 bytes
1package httpd
2
3import (
4 "fmt"
5 "net/http"
6 "strings"
7)
8
9// Status badges. A badge is a small SVG served for public repositories,
10// so a README on any host can show whether the latest build passed.
11// Private repositories 404 like everywhere else — a badge must not leak
12// that a repo exists, let alone its state.
13
14// badgeColors are the shield fills per build state.
15var badgeColors = map[string]string{
16 "success": "#2da44e",
17 "failure": "#cf222e",
18 "running": "#bf8700",
19 "pending": "#bf8700",
20 "unknown": "#6b7280",
21}
22
23// badgeWidth approximates Verdana 11px advance so the pill fits its text
24// without shipping a font metric table.
25func badgeWidth(s string) int {
26 w := 0
27 for _, r := range s {
28 switch {
29 case strings.ContainsRune("iljtIJ1.,:;'", r):
30 w += 4
31 case strings.ContainsRune("mwMW", r):
32 w += 10
33 case r >= 'A' && r <= 'Z':
34 w += 8
35 default:
36 w += 7
37 }
38 }
39 return w + 10
40}
41
42// badgeSVG renders a two-part pill: a grey label and a coloured state.
43func badgeSVG(label, state string) string {
44 color := badgeColors[state]
45 if color == "" {
46 color = badgeColors["unknown"]
47 }
48 lw, sw := badgeWidth(label), badgeWidth(state)
49 total := lw + sw
50 return fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="20" role="img" aria-label="%s: %s">
51<title>%s: %s</title>
52<rect width="%d" height="20" rx="3" fill="#444d56"/>
53<rect x="%d" width="%d" height="20" rx="3" fill="%s"/>
54<rect x="%d" width="4" height="20" fill="%s"/>
55<g fill="#fff" text-anchor="middle" font-family="Verdana,DejaVu Sans,sans-serif" font-size="11">
56<text x="%d" y="14">%s</text>
57<text x="%d" y="14">%s</text>
58</g>
59</svg>`, total, label, state, label, state,
60 total, lw, sw, color, lw, color, lw/2, label, lw+sw/2, state)
61}
62
63// buildBadge answers GET /{owner}/{repo}/badge/build.svg[?job=name].
64func (s *Server) buildBadge(w http.ResponseWriter, r *http.Request) {
65 repo, ok := s.publicRepo(r.PathValue("owner"), strings.TrimSuffix(r.PathValue("repo"), ".git"))
66 if !ok {
67 http.NotFound(w, r)
68 return
69 }
70 job := r.URL.Query().Get("job")
71 label := "build"
72 if job != "" {
73 label = job
74 }
75 state := "unknown"
76 if b, err := s.st.LatestBuild(repo.ID, job); err == nil {
77 state = b.Status
78 }
79 writeBadge(w, label, state)
80}
81
82func writeBadge(w http.ResponseWriter, label, state string) {
83 w.Header().Set("Content-Type", "image/svg+xml; charset=utf-8")
84 // Badges are read by other people's caches; keep them briefly fresh
85 // rather than pinned to a stale result.
86 w.Header().Set("Cache-Control", "max-age=60, must-revalidate")
87 w.Header().Set("X-Content-Type-Options", "nosniff")
88 fmt.Fprint(w, badgeSVG(label, state))
89}