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 | 139 | t.Fatalf("host runners:\n%s", out) |
| 140 | 140 | } |
| 141 | 141 | } |
| 142 | |
| 143 | // /healthz is unauthenticated, cache-free, and says which build serves. |
| 144 | func 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 @@ |
| 1 | package httpd |
| 2 | |
| 3 | import ( |
| 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. |
| 14 | func (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 | 38 | routes = append(routes, |
| 39 | 39 | Route{Method: "GET", Pattern: "/{$}", Handler: s.index}, |
| 40 | 40 | Route{Method: "GET", Pattern: "/explore", Handler: s.explore}, |
| 41 | Route{Method: "GET", Pattern: "/healthz", Handler: s.healthz}, |
| 41 | 42 | Route{Method: "GET", Pattern: "/privacy", Handler: s.privacy}, |
| 42 | 43 | Route{Method: "GET", Pattern: "/static/style.css", Handler: s.stylesheet}, |
| 43 | 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 | 18 | "favicon.svg": true, |
| 19 | 19 | "gitbay": true, // vanity go-import path on gitbay.org |
| 20 | 20 | "gitbay-bot": true, // authors dependency-update issues |
| 21 | "healthz": true, |
| 21 | 22 | "login": true, |
| 22 | 23 | "logout": true, |
| 23 | 24 | "new": true, |