internal/httpd/badge.go
117 lines · 3436 bytes
7 symbols in this file
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 "cancelled": "#6b7280",
21 "unknown": "#6b7280",
22}
23
24// badgeWidth approximates Verdana 11px advance so the pill fits its text
25// without shipping a font metric table.
26func badgeWidth(s string) int {
27 w := 0
28 for _, r := range s {
29 switch {
30 case strings.ContainsRune("iljtIJ1.,:;'", r):
31 w += 4
32 case strings.ContainsRune("mwMW", r):
33 w += 10
34 case r >= 'A' && r <= 'Z':
35 w += 8
36 default:
37 w += 7
38 }
39 }
40 return w + 10
41}
42
43// badgeSVG renders a two-part pill: a grey label and a coloured state.
44func badgeSVG(label, state string) string {
45 color := badgeColors[state]
46 if color == "" {
47 color = badgeColors["unknown"]
48 }
49 lw, sw := badgeWidth(label), badgeWidth(state)
50 total := lw + sw
51 return fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="20" role="img" aria-label="%s: %s">
52<title>%s: %s</title>
53<rect width="%d" height="20" rx="3" fill="#444d56"/>
54<rect x="%d" width="%d" height="20" rx="3" fill="%s"/>
55<rect x="%d" width="4" height="20" fill="%s"/>
56<g fill="#fff" text-anchor="middle" font-family="Verdana,DejaVu Sans,sans-serif" font-size="11">
57<text x="%d" y="14">%s</text>
58<text x="%d" y="14">%s</text>
59</g>
60</svg>`, total, label, state, label, state,
61 total, lw, sw, color, lw, color, lw/2, label, lw+sw/2, state)
62}
63
64// buildBadge answers GET /{owner}/{repo}/badge/build.svg[?job=name].
65func (s *Server) buildBadge(w http.ResponseWriter, r *http.Request) {
66 label, state, ok := s.badgeState(r)
67 if !ok {
68 http.NotFound(w, r)
69 return
70 }
71 badgeHeaders(w, "image/svg+xml; charset=utf-8")
72 fmt.Fprint(w, badgeSVG(label, state))
73}
74
75// buildBadgePNG answers GET /{owner}/{repo}/badge/build.png[?job=name], for
76// readers that cannot decode SVG.
77func (s *Server) buildBadgePNG(w http.ResponseWriter, r *http.Request) {
78 label, state, ok := s.badgeState(r)
79 if !ok {
80 http.NotFound(w, r)
81 return
82 }
83 out, err := badgePNG(label, state)
84 if err != nil {
85 http.Error(w, "badge unavailable", http.StatusInternalServerError)
86 return
87 }
88 badgeHeaders(w, "image/png")
89 w.Write(out)
90}
91
92// badgeState resolves the repo and its latest build. Private repositories
93// report not ok — a badge must not leak that a repo exists.
94func (s *Server) badgeState(r *http.Request) (label, state string, ok bool) {
95 repo, ok := s.publicRepo(r.PathValue("owner"), strings.TrimSuffix(r.PathValue("repo"), ".git"))
96 if !ok {
97 return "", "", false
98 }
99 job := r.URL.Query().Get("job")
100 label = "build"
101 if job != "" {
102 label = job
103 }
104 state = "unknown"
105 if b, err := s.st.LatestBuild(repo.ID, job); err == nil {
106 state = b.Status
107 }
108 return label, state, true
109}
110
111func badgeHeaders(w http.ResponseWriter, contentType string) {
112 w.Header().Set("Content-Type", contentType)
113 // Badges are read by other people's caches; keep them briefly fresh
114 // rather than pinned to a stale result.
115 w.Header().Set("Cache-Control", "max-age=60, must-revalidate")
116 w.Header().Set("X-Content-Type-Options", "nosniff")
117}