Commit badc110670

badc110670d80cc0727d187b52638f3b27c9357d

parent: 7b6823e3e9

Verified · cmc ci/build: success ci/test: success ci/vuln: success

cmc <hello@cleberg.net> · 2026-09-02 02:52 UTC

http: /healthz

The host monitor checked systemd, disk and the certificate from outside
the process. Nothing reported from inside it.

GET /healthz answers with no auth and no repository data: whether the
database answers, and the commit serving. 503 when it does not, so a
checker reading only the status code still learns the truth. healthz
joins the reserved names.

Closes #77

Layout: unified · split

e2e/reap_test.go +13
@@ -139,3 +139,16 @@ func TestAdminRunners(t *testing.T) {
139 t.Fatalf("host runners:\n%s", out) 139 t.Fatalf("host runners:\n%s", out)
140 } 140 }
141} 141}
142
143// /healthz is unauthenticated, cache-free, and says which build serves.
144func TestHealthz(t *testing.T) {
145 inst := startInstance(t)
146 status, body := inst.get(t, "/healthz")
147 if status != 200 || !strings.Contains(body, `"ok":true`) || !strings.Contains(body, `"commit":"`) {
148 t.Fatalf("healthz: %d %s", status, body)
149 }
150 // The name is reserved: no account can shadow the route.
151 if _, errOut, code := inst.ssh(t, inst.newKey(t, "x"), "", "register", "--username", "healthz"); code == 0 {
152 t.Fatalf("healthz registered as a username: %s", errOut)
153 }
154}
internal/httpd/healthz.go added +31
@@ -0,0 +1,31 @@
1package httpd
2
3import (
4 "encoding/json"
5 "net/http"
6
7 "gitbay.org/gitbay/internal/buildinfo"
8)
9
10// healthz answers from inside the process: whether the database answers,
11// and which build is serving. No auth, no repository data; a monitor or
12// an uptime check reads it. 503 when the database does not answer, so a
13// checker that only reads the status code still learns the truth.
14func (s *Server) healthz(w http.ResponseWriter, r *http.Request) {
15 dbErr := ""
16 if err := s.st.DB.Ping(); err != nil {
17 dbErr = err.Error()
18 } else if _, err := s.st.Version(); err != nil {
19 dbErr = err.Error()
20 }
21 w.Header().Set("Content-Type", "application/json")
22 w.Header().Set("Cache-Control", "no-store")
23 if dbErr != "" {
24 w.WriteHeader(http.StatusServiceUnavailable)
25 }
26 json.NewEncoder(w).Encode(map[string]any{
27 "ok": dbErr == "",
28 "commit": buildinfo.String(),
29 "db": map[string]any{"ok": dbErr == "", "error": dbErr},
30 })
31}
internal/httpd/routes.go +1
@@ -38,6 +38,7 @@ func (s *Server) Routes() []Route {
38 routes = append(routes, 38 routes = append(routes,
39 Route{Method: "GET", Pattern: "/{$}", Handler: s.index}, 39 Route{Method: "GET", Pattern: "/{$}", Handler: s.index},
40 Route{Method: "GET", Pattern: "/explore", Handler: s.explore}, 40 Route{Method: "GET", Pattern: "/explore", Handler: s.explore},
41 Route{Method: "GET", Pattern: "/healthz", Handler: s.healthz},
41 Route{Method: "GET", Pattern: "/privacy", Handler: s.privacy}, 42 Route{Method: "GET", Pattern: "/privacy", Handler: s.privacy},
42 Route{Method: "GET", Pattern: "/static/style.css", Handler: s.stylesheet}, 43 Route{Method: "GET", Pattern: "/static/style.css", Handler: s.stylesheet},
43 // Literal per-file routes: a {name} wildcard is ambiguous against 44 // Literal per-file routes: a {name} wildcard is ambiguous against
internal/policy/names.go +1
@@ -18,6 +18,7 @@ var reservedNames = map[string]bool{
18 "favicon.svg": true, 18 "favicon.svg": true,
19 "gitbay": true, // vanity go-import path on gitbay.org 19 "gitbay": true, // vanity go-import path on gitbay.org
20 "gitbay-bot": true, // authors dependency-update issues 20 "gitbay-bot": true, // authors dependency-update issues
21 "healthz": true,
21 "login": true, 22 "login": true,
22 "logout": true, 23 "logout": true,
23 "new": true, 24 "new": true,