A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit eb453997f3

eb453997f3dc762d6032263eca7367c6d1097ee8

parent: b685adf5ab

Verified · cmc ci/build: success

cmc <hello@cleberg.net> · 2026-08-30T04:04:29Z

gitbayd: warn at startup about a build that cannot be vouched for

Two warnings, both on the serve path.

An uncommitted build is a WARN rather than an INFO: it came from a tree
that was never committed, so nothing can say what is running. Both of
this week's stale binaries looked exactly like that.

A build that is committed but not on the source repository's default
branch gets its own warning. Such a build serves perfectly well, which
is why it can sit unnoticed. The check needs to know which hosted
repository the instance develops itself in, so it reads
server.source_repo and stays silent when that is unset. HEAD in a bare
repository is the default branch, so there is no branch name to resolve.

A commit the repository has never seen warns too: unverifiable is not
the same as fine.

Ref #28.
cmd/gitbayd/main.go +2 −2
@@ -18,7 +18,6 @@ import (
1818 "golang.org/x/crypto/acme/autocert"
1919 "golang.org/x/crypto/ssh"
2020
21 "gitbay.org/gitbay/internal/buildinfo"
2221 "gitbay.org/gitbay/internal/ci"
2322 "gitbay.org/gitbay/internal/config"
2423 "gitbay.org/gitbay/internal/control"
@@ -120,11 +119,12 @@ func serveCmd() *cobra.Command {
120119 RunE: func(cmd *cobra.Command, args []string) error {
121120 // First line of every run: the journal then says which commit is
122121 // serving, without rebuilding the binary to find out.
123 slog.Info("gitbayd starting", "commit", buildinfo.String())
122 logBuild()
124123 cfg, err := config.Load(configPath)
125124 if err != nil {
126125 return err
127126 }
127 warnIfUnmerged(cfg)
128128 st, err := openStore(cfg)
129129 if err != nil {
130130 return err
cmd/gitbayd/version.go +49
@@ -2,10 +2,15 @@ package main
22
33 import (
44 "fmt"
5 "log/slog"
6 "strings"
57
68 "github.com/spf13/cobra"
79
810 "gitbay.org/gitbay/internal/buildinfo"
11 "gitbay.org/gitbay/internal/config"
12 "gitbay.org/gitbay/internal/control"
13 "gitbay.org/gitbay/internal/gitutil"
914 )
1015
1116 func versionCmd() *cobra.Command {
@@ -19,3 +24,47 @@ func versionCmd() *cobra.Command {
1924 },
2025 }
2126 }
27
28// logBuild announces the running build. An unidentified one is a warning
29// rather than a fact: it was built from a tree that was never committed, so
30// the source it came from no longer exists anywhere.
31func logBuild() {
32 if !buildinfo.Identified() {
33 slog.Warn("gitbayd starting from an uncommitted build", "commit", buildinfo.String())
34 return
35 }
36 slog.Info("gitbayd starting", "commit", buildinfo.String())
37}
38
39// warnIfUnmerged says so when the running build is not on the default branch
40// of the repository this instance develops itself in. Such a build serves
41// perfectly well, which is exactly why it can sit unnoticed for days.
42//
43// Silent unless server.source_repo is set, since an instance that does not
44// host its own source has nothing to check against.
45func warnIfUnmerged(cfg config.Config) {
46 repo := cfg.Server.SourceRepo
47 if repo == "" || !buildinfo.Identified() {
48 return
49 }
50 owner, name, ok := strings.Cut(repo, "/")
51 if !ok || owner == "" || name == "" {
52 slog.Warn("server.source_repo is not owner/name; skipping the build check", "source_repo", repo)
53 return
54 }
55 // HEAD in a bare repository is the default branch, so there is no branch
56 // name to resolve or configure.
57 dir := control.RepoDir(cfg.Server.Root, owner, name)
58 onBranch, err := gitutil.IsAncestor(dir, buildinfo.String(), "HEAD")
59 if err != nil {
60 // An unpushed commit lands here too, and is worth the same warning:
61 // it cannot be checked, so it cannot be vouched for.
62 slog.Warn("cannot check this build against the source repository",
63 "commit", buildinfo.String(), "repo", repo, "err", err)
64 return
65 }
66 if !onBranch {
67 slog.Warn("this build is not on the source repository's default branch",
68 "commit", buildinfo.String(), "repo", repo)
69 }
70}
cmd/gitbayd/version_test.go added +121
@@ -0,0 +1,121 @@
1package main
2
3import (
4 "bytes"
5 "log/slog"
6 "os/exec"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "gitbay.org/gitbay/internal/buildinfo"
12 "gitbay.org/gitbay/internal/config"
13 "gitbay.org/gitbay/internal/control"
14)
15
16func capture(t *testing.T, fn func()) string {
17 t.Helper()
18 var buf bytes.Buffer
19 prev := slog.Default()
20 slog.SetDefault(slog.New(slog.NewTextHandler(&buf, nil)))
21 defer slog.SetDefault(prev)
22 fn()
23 return buf.String()
24}
25
26func TestLogBuildWarnsOnAnUncommittedBuild(t *testing.T) {
27 prev := buildinfo.Commit
28 defer func() { buildinfo.Commit = prev }()
29
30 buildinfo.Commit = "abc123abc123"
31 if out := capture(t, logBuild); !strings.Contains(out, "level=INFO") {
32 t.Errorf("a committed build should log INFO:\n%s", out)
33 }
34
35 buildinfo.Commit = "abc123abc123-dirty"
36 out := capture(t, logBuild)
37 if !strings.Contains(out, "level=WARN") {
38 t.Errorf("a dirty build should log WARN:\n%s", out)
39 }
40 if !strings.Contains(out, "abc123abc123-dirty") {
41 t.Errorf("the warning should name the build:\n%s", out)
42 }
43}
44
45// sourceRepo builds a bare repository holding one commit on the default
46// branch, plus one commit off it, and returns the config pointing at it.
47func sourceRepo(t *testing.T) (cfg config.Config, onBranch, offBranch string) {
48 t.Helper()
49 root := t.TempDir()
50 dir := control.RepoDir(root, "krz", "gitbay")
51
52 work := filepath.Join(t.TempDir(), "work")
53 run := func(args ...string) string {
54 t.Helper()
55 cmd := exec.Command("git", args...)
56 cmd.Env = append(cmd.Environ(),
57 "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@e", "GIT_AUTHOR_DATE=2026-01-01T00:00:00Z",
58 "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@e", "GIT_COMMITTER_DATE=2026-01-01T00:00:00Z")
59 out, err := cmd.CombinedOutput()
60 if err != nil {
61 t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
62 }
63 return strings.TrimSpace(string(out))
64 }
65
66 run("init", "-q", "-b", "main", work)
67 run("-C", work, "commit", "-q", "--allow-empty", "-m", "on the branch")
68 onBranch = run("-C", work, "rev-parse", "HEAD")
69 run("-C", work, "checkout", "-q", "-b", "side")
70 run("-C", work, "commit", "-q", "--allow-empty", "-m", "off the branch")
71 offBranch = run("-C", work, "rev-parse", "HEAD")
72
73 run("clone", "-q", "--bare", work, dir)
74 run("-C", dir, "symbolic-ref", "HEAD", "refs/heads/main")
75
76 cfg = config.Config{Server: config.Server{Root: root, SourceRepo: "krz/gitbay"}}
77 return cfg, onBranch, offBranch
78}
79
80func TestWarnIfUnmerged(t *testing.T) {
81 prev := buildinfo.Commit
82 defer func() { buildinfo.Commit = prev }()
83
84 cfg, onBranch, offBranch := sourceRepo(t)
85
86 buildinfo.Commit = onBranch
87 if out := capture(t, func() { warnIfUnmerged(cfg) }); out != "" {
88 t.Errorf("a build on the default branch should be silent:\n%s", out)
89 }
90
91 buildinfo.Commit = offBranch
92 out := capture(t, func() { warnIfUnmerged(cfg) })
93 if !strings.Contains(out, "not on the source repository") {
94 t.Errorf("a build off the default branch should warn:\n%s", out)
95 }
96
97 // A commit the repository has never seen cannot be vouched for.
98 buildinfo.Commit = "0123456789abcdef0123456789abcdef01234567"
99 if out := capture(t, func() { warnIfUnmerged(cfg) }); !strings.Contains(out, "cannot check") {
100 t.Errorf("an unknown commit should warn:\n%s", out)
101 }
102}
103
104func TestWarnIfUnmergedIsSilentWithoutASourceRepo(t *testing.T) {
105 prev := buildinfo.Commit
106 defer func() { buildinfo.Commit = prev }()
107 buildinfo.Commit = "abc123abc123"
108
109 // The default for every instance that does not host its own source.
110 cfg := config.Config{Server: config.Server{Root: t.TempDir()}}
111 if out := capture(t, func() { warnIfUnmerged(cfg) }); out != "" {
112 t.Errorf("no source_repo should mean no check:\n%s", out)
113 }
114
115 // A dirty build has already been warned about by logBuild; do not warn twice.
116 cfg.Server.SourceRepo = "krz/gitbay"
117 buildinfo.Commit = "abc123abc123-dirty"
118 if out := capture(t, func() { warnIfUnmerged(cfg) }); out != "" {
119 t.Errorf("a dirty build is logBuild's to report, not this one's:\n%s", out)
120 }
121}
internal/buildinfo/buildinfo.go +12 −1
@@ -3,7 +3,10 @@
33 // a hash comparison to infer it.
44 package buildinfo
55
6import "runtime/debug"
6import (
7 "runtime/debug"
8 "strings"
9)
710
811 // Commit is stamped at link time by the Makefile:
912 //
@@ -43,3 +46,11 @@ func String() string {
4346 }
4447 return rev + modified
4548 }
49
50// Identified reports whether this build can be traced back to a commit that
51// exists in history. A dirty or missing stamp means it cannot: the tree it was
52// built from was never committed, so nothing can say what is running.
53func Identified() bool {
54 s := String()
55 return s != "unknown" && !strings.HasSuffix(s, "-dirty")
56}
internal/buildinfo/buildinfo_test.go +19
@@ -54,3 +54,22 @@ func TestMakefileStampMatchesHEAD(t *testing.T) {
5454 t.Errorf("String() = %q, but HEAD is %q", got, want)
5555 }
5656 }
57
58func TestIdentified(t *testing.T) {
59 prev := Commit
60 defer func() { Commit = prev }()
61
62 for _, c := range []struct {
63 stamp string
64 want bool
65 }{
66 {"b685adf5ab7a", true},
67 {"b685adf5ab7a-dirty", false},
68 {"unknown", false},
69 } {
70 Commit = c.stamp
71 if got := Identified(); got != c.want {
72 t.Errorf("Identified() with stamp %q = %v, want %v", c.stamp, got, c.want)
73 }
74 }
75}
internal/config/config.go +6
@@ -35,6 +35,12 @@ type Config struct {
3535 type Server struct {
3636 Root string `toml:"root"`
3737 SiteURL string `toml:"site_url"`
38
39 // SourceRepo names the repository this instance develops itself in, as
40 // "owner/name". When set, startup warns if the running build's commit is
41 // not on that repository's default branch. Empty disables the check, which
42 // is right for any instance that does not host its own source.
43 SourceRepo string `toml:"source_repo"`
3844 }
3945
4046 type SSH struct {