Commit eb453997f3
Verified · cmc ci/build: success
cmd/gitbayd/main.go +2 −2
| @@ -18,7 +18,6 @@ import ( | ||
| 18 | 18 | "golang.org/x/crypto/acme/autocert" |
| 19 | 19 | "golang.org/x/crypto/ssh" |
| 20 | 20 | |
| 21 | "gitbay.org/gitbay/internal/buildinfo" | |
| 22 | 21 | "gitbay.org/gitbay/internal/ci" |
| 23 | 22 | "gitbay.org/gitbay/internal/config" |
| 24 | 23 | "gitbay.org/gitbay/internal/control" |
| @@ -120,11 +119,12 @@ func serveCmd() *cobra.Command { | ||
| 120 | 119 | RunE: func(cmd *cobra.Command, args []string) error { |
| 121 | 120 | // First line of every run: the journal then says which commit is |
| 122 | 121 | // serving, without rebuilding the binary to find out. |
| 123 | slog.Info("gitbayd starting", "commit", buildinfo.String()) | |
| 122 | logBuild() | |
| 124 | 123 | cfg, err := config.Load(configPath) |
| 125 | 124 | if err != nil { |
| 126 | 125 | return err |
| 127 | 126 | } |
| 127 | warnIfUnmerged(cfg) | |
| 128 | 128 | st, err := openStore(cfg) |
| 129 | 129 | if err != nil { |
| 130 | 130 | return err |
cmd/gitbayd/version.go +49
| @@ -2,10 +2,15 @@ package main | ||
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | 4 | "fmt" |
| 5 | "log/slog" | |
| 6 | "strings" | |
| 5 | 7 | |
| 6 | 8 | "github.com/spf13/cobra" |
| 7 | 9 | |
| 8 | 10 | "gitbay.org/gitbay/internal/buildinfo" |
| 11 | "gitbay.org/gitbay/internal/config" | |
| 12 | "gitbay.org/gitbay/internal/control" | |
| 13 | "gitbay.org/gitbay/internal/gitutil" | |
| 9 | 14 | ) |
| 10 | 15 | |
| 11 | 16 | func versionCmd() *cobra.Command { |
| @@ -19,3 +24,47 @@ func versionCmd() *cobra.Command { | ||
| 19 | 24 | }, |
| 20 | 25 | } |
| 21 | 26 | } |
| 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. | |
| 31 | func 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. | |
| 45 | func 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 @@ | ||
| 1 | package main | |
| 2 | ||
| 3 | import ( | |
| 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 | ||
| 16 | func 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 | ||
| 26 | func 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. | |
| 47 | func 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 | ||
| 80 | func 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 | ||
| 104 | func 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 @@ | ||
| 3 | 3 | // a hash comparison to infer it. |
| 4 | 4 | package buildinfo |
| 5 | 5 | |
| 6 | import "runtime/debug" | |
| 6 | import ( | |
| 7 | "runtime/debug" | |
| 8 | "strings" | |
| 9 | ) | |
| 7 | 10 | |
| 8 | 11 | // Commit is stamped at link time by the Makefile: |
| 9 | 12 | // |
| @@ -43,3 +46,11 @@ func String() string { | ||
| 43 | 46 | } |
| 44 | 47 | return rev + modified |
| 45 | 48 | } |
| 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. | |
| 53 | func 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) { | ||
| 54 | 54 | t.Errorf("String() = %q, but HEAD is %q", got, want) |
| 55 | 55 | } |
| 56 | 56 | } |
| 57 | ||
| 58 | func 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 { | ||
| 35 | 35 | type Server struct { |
| 36 | 36 | Root string `toml:"root"` |
| 37 | 37 | 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"` | |
| 38 | 44 | } |
| 39 | 45 | |
| 40 | 46 | type SSH struct { |