Commit f89140d141
Verified · cmc
CHANGELOG.org added +54
| @@ -0,0 +1,54 @@ | ||
| 1 | #+title: gitbay changelog | |
| 2 | ||
| 3 | Versioning follows semver from v0.1.0. Database migrations run | |
| 4 | automatically on daemon start; upgrade notes appear per release when | |
| 5 | anything beyond "replace the binary and restart" is needed. | |
| 6 | ||
| 7 | * Unreleased | |
| 8 | ||
| 9 | Deploy keys: repo-bound CI keys (=repo deploy-key=), ro/rw, rename- and | |
| 10 | transfer-proof. | |
| 11 | Commit statuses (=status set/list=), combined state on MR pages, and a | |
| 12 | =require-checks= merge gate. | |
| 13 | Email notifications for issue and MR activity (participants with | |
| 14 | verified addresses; never the actor). | |
| 15 | Inline review threads on MR diffs (=mr diff-comment/threads/resolve=), | |
| 16 | stale on force-push, =require-resolved= merge gate. | |
| 17 | Required approvals with CODEOWNERS (=require-approvals=; latest review | |
| 18 | wins, author excluded) and a =require-resolved= gate; merge gate order | |
| 19 | is checks → approvals → CODEOWNERS → resolved threads → signatures. | |
| 20 | Web design revamp: token-based stylesheet (light+dark), wordmark and | |
| 21 | favicon, aligned layout grid, card-based listings, designed 404, | |
| 22 | mobile pass. | |
| 23 | Cross-references and mentions: =#N=, =!N=, =owner/name#N=, =@user= | |
| 24 | autolink in issue/MR text, viewer-aware for private repos. | |
| 25 | Archived repositories (read-only with badge) and repo topics. | |
| 26 | Blame view with signature-aware attribution and 1000-line pages. | |
| 27 | Repository search (name/description/topic) and per-repo code search | |
| 28 | (=repo grep=, web search tab); blob line anchors. | |
| 29 | Homepage: dashboard for logged-in users (pinned repos via =repo pin=, | |
| 30 | open MRs and issues involving you), landing page for visitors, full | |
| 31 | public listing at =/explore=; MR diffs collapsed by default. | |
| 32 | Milestones (=milestone create/list/close=, =issue/mr milestone=) with | |
| 33 | web progress; issue templates from =.gitbay/issue-template*.md= | |
| 34 | (CLI =$EDITOR= prefill and web form). | |
| 35 | Issue actions from commit messages landing on the default branch: | |
| 36 | =closes/fixes/resolves #N= closes, bare =#N= leaves a reference | |
| 37 | comment; once per issue+commit. | |
| 38 | Vanity Go imports: =[go_import]= config serves go-import meta tags, so | |
| 39 | =go install gitbay.org/gitbay/cmd/...@latest= works. | |
| 40 | Release script: =deploy/release.sh <tag>= builds reproducible | |
| 41 | linux/amd64, linux/arm64, and darwin/arm64 binaries with SHA256SUMS. | |
| 42 | ||
| 43 | Upgrade notes: migrations 0006–0014 apply on start. No config changes | |
| 44 | required; =[go_import]= and =web.mode = "accounts"= are opt-in. | |
| 45 | ||
| 46 | * v0.1.0 — 2026-08-24 | |
| 47 | ||
| 48 | First tagged release: the complete CLI-first forge. SSH control plane | |
| 49 | (bare-OpenSSH usable), git over SSH/HTTPS/git-daemon, repos, issues, | |
| 50 | merge requests (ff/merge/squash/rebase with signature policy), OpenPGP | |
| 51 | and SSHSIG verification with retroactive re-verification, orgs, repo | |
| 52 | import, web UI (view-only or accounts mode), registration with invites | |
| 53 | and SMTP, HTTPS/JSON API with SSH-minted tokens, webhooks, backups, | |
| 54 | ACME TLS, systemd deployment. | |
deploy/release.sh added +38
| @@ -0,0 +1,38 @@ | ||
| 1 | #!/bin/sh | |
| 2 | # Build release binaries for a tag: reproducible cross-compiled gitbay and | |
| 3 | # gitbayd with a checksum manifest. | |
| 4 | # | |
| 5 | # git checkout v0.2.0 && ./deploy/release.sh v0.2.0 | |
| 6 | # | |
| 7 | # Reproducibility: CGO off, -trimpath, stripped, empty build id; the VCS | |
| 8 | # revision embedded by the toolchain is deterministic per commit. Anyone on | |
| 9 | # the same Go toolchain and commit gets byte-identical binaries. | |
| 10 | set -eu | |
| 11 | ||
| 12 | V="${1:-}" | |
| 13 | [ -n "$V" ] || { echo "usage: $0 <version-tag>" >&2; exit 2; } | |
| 14 | ||
| 15 | out="dist/release/$V" | |
| 16 | rm -rf "$out" | |
| 17 | mkdir -p "$out" | |
| 18 | ||
| 19 | for target in linux/amd64 linux/arm64 darwin/arm64; do | |
| 20 | goos="${target%/*}" | |
| 21 | goarch="${target#*/}" | |
| 22 | for bin in gitbay gitbayd; do | |
| 23 | name="${bin}-${V}-${goos}-${goarch}" | |
| 24 | echo "building $name" | |
| 25 | CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" \ | |
| 26 | go build -trimpath -ldflags='-s -w -buildid=' \ | |
| 27 | -o "$out/$name" "./cmd/$bin" | |
| 28 | done | |
| 29 | done | |
| 30 | ||
| 31 | cd "$out" | |
| 32 | if command -v sha256sum >/dev/null 2>&1; then | |
| 33 | sha256sum -- * > SHA256SUMS | |
| 34 | else | |
| 35 | shasum -a 256 -- * > SHA256SUMS | |
| 36 | fi | |
| 37 | echo "wrote $out/SHA256SUMS" | |
| 38 | cat SHA256SUMS | |
docs/admin.org +14
| @@ -6,6 +6,13 @@ startup and on every admin command. | ||
| 6 | 6 | |
| 7 | 7 | * Install |
| 8 | 8 | |
| 9 | Build from source (=go build ./cmd/gitbayd=), install via the vanity | |
| 10 | module path (=go install gitbay.org/gitbay/cmd/gitbayd@latest=), or use | |
| 11 | a release build: =deploy/release.sh <tag>= cross-compiles reproducible | |
| 12 | linux/amd64, linux/arm64, and darwin/arm64 binaries with a SHA256SUMS | |
| 13 | manifest (CGO off, trimpath, stripped — byte-identical per commit and | |
| 14 | toolchain). | |
| 15 | ||
| 9 | 16 | #+begin_src sh |
| 10 | 17 | install -m 755 gitbayd /usr/local/bin/ |
| 11 | 18 | adduser --system --group --home /var/lib/gitbay --shell /usr/sbin/nologin gitbay |
| @@ -95,6 +102,13 @@ contradiction; =--no-host-checks= skips port/path probes. | ||
| 95 | 102 | Serves only public repositories that additionally ran |
| 96 | 103 | =repo settings git-daemon <repo> on=. |
| 97 | 104 | |
| 105 | ** [go_import] | |
| 106 | Vanity Go module paths, one per line: ="host/module" = "owner/repo"=. | |
| 107 | Requests with =?go-get=1= at or under the module path answer with the | |
| 108 | go-import meta tag pointing at the repository's HTTPS clone URL, so | |
| 109 | =go install host/module/cmd/...@latest= resolves. The repository should | |
| 110 | be public (the module path itself confirms it exists). | |
| 111 | ||
| 98 | 112 | * Users, email, invites |
| 99 | 113 | |
| 100 | 114 | #+begin_src sh |
docs/users.org +10
| @@ -5,6 +5,16 @@ Everything here works from stock OpenSSH — replace =gitbay= with | ||
| 5 | 5 | convenience (instance profiles, repo inference, =$EDITOR=), nothing more. |
| 6 | 6 | =ssh git@<host> help= lists every command the server knows. |
| 7 | 7 | |
| 8 | * Installing the CLI | |
| 9 | ||
| 10 | #+begin_src sh | |
| 11 | go install gitbay.org/gitbay/cmd/gitbay@latest # any platform with Go | |
| 12 | brew install krz/tap/gitbay # Homebrew (macOS/Linux) | |
| 13 | #+end_src | |
| 14 | ||
| 15 | Or build from source: =go build ./cmd/gitbay= in a clone of | |
| 16 | =https://gitbay.org/krz/gitbay.git=. | |
| 17 | ||
| 8 | 18 | * Getting an account |
| 9 | 19 | |
| 10 | 20 | How you join depends on the instance's registration mode: |
e2e/goimport_test.go added +34
| @@ -0,0 +1,34 @@ | ||
| 1 | package e2e | |
| 2 | ||
| 3 | import ( | |
| 4 | "strings" | |
| 5 | "testing" | |
| 6 | ) | |
| 7 | ||
| 8 | func TestGoImportVanity(t *testing.T) { | |
| 9 | inst := startInstanceWith(t, "[go_import]\n\"127.0.0.1/tool\" = \"alice/tool\"\n") | |
| 10 | aliceKey := inst.newKey(t, "alice") | |
| 11 | inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub") | |
| 12 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/tool"); code != 0 { | |
| 13 | t.Fatalf("repo create: %s", errOut) | |
| 14 | } | |
| 15 | ||
| 16 | want := `<meta name="go-import" content="127.0.0.1/tool git https://gitbay.test/alice/tool.git">` | |
| 17 | status, body := inst.get(t, "/tool?go-get=1") | |
| 18 | if status != 200 || !strings.Contains(body, want) { | |
| 19 | t.Fatalf("module root: %d\n%s", status, body) | |
| 20 | } | |
| 21 | // Subpackages resolve to the same module. | |
| 22 | status, body = inst.get(t, "/tool/cmd/x?go-get=1") | |
| 23 | if status != 200 || !strings.Contains(body, want) { | |
| 24 | t.Fatalf("subpackage: %d\n%s", status, body) | |
| 25 | } | |
| 26 | // Prefix boundary: /toolbox is not under the module. | |
| 27 | if _, body = inst.get(t, "/toolbox?go-get=1"); strings.Contains(body, "go-import") { | |
| 28 | t.Fatal("prefix leak: /toolbox matched") | |
| 29 | } | |
| 30 | // Without go-get the path routes normally (no owner "tool" -> 404). | |
| 31 | if status, _ = inst.get(t, "/tool"); status != 404 { | |
| 32 | t.Fatalf("normal routing broken: %d", status) | |
| 33 | } | |
| 34 | } | |
internal/config/config.go +14
| @@ -23,6 +23,10 @@ type Config struct { | ||
| 23 | 23 | Webhooks Webhooks `toml:"webhooks"` |
| 24 | 24 | Limits Limits `toml:"limits"` |
| 25 | 25 | Mail Mail `toml:"mail"` |
| 26 | // GoImport maps vanity Go module paths to repositories, e.g. | |
| 27 | // "gitbay.org/gitbay" = "krz/gitbay". Requests carrying ?go-get=1 | |
| 28 | // under a mapped path get a go-import meta tag. | |
| 29 | GoImport map[string]string `toml:"go_import"` | |
| 26 | 30 | } |
| 27 | 31 | |
| 28 | 32 | type Server struct { |
| @@ -171,6 +175,16 @@ func (c Config) Validate() error { | ||
| 171 | 175 | errs = append(errs, err) |
| 172 | 176 | } |
| 173 | 177 | |
| 178 | for module, repo := range c.GoImport { | |
| 179 | host, _, ok := strings.Cut(module, "/") | |
| 180 | if !ok || host == "" || !strings.Contains(host, ".") { | |
| 181 | errs = append(errs, fmt.Errorf("go_import key %q must be host/path (e.g. gitbay.org/gitbay)", module)) | |
| 182 | } | |
| 183 | if parts := strings.Split(repo, "/"); len(parts) != 2 || parts[0] == "" || parts[1] == "" { | |
| 184 | errs = append(errs, fmt.Errorf("go_import value %q must be owner/name", repo)) | |
| 185 | } | |
| 186 | } | |
| 187 | ||
| 174 | 188 | // Contradictions. |
| 175 | 189 | if c.Mail.SMTPHost != "" && c.Mail.From == "" { |
| 176 | 190 | errs = append(errs, errors.New("[mail] from is required when smtp_host is set")) |
internal/httpd/routes.go +29 −2
| @@ -1,6 +1,11 @@ | ||
| 1 | 1 | package httpd |
| 2 | 2 | |
| 3 | import "net/http" | |
| 3 | import ( | |
| 4 | "fmt" | |
| 5 | "net" | |
| 6 | "net/http" | |
| 7 | "strings" | |
| 8 | ) | |
| 4 | 9 | |
| 5 | 10 | // Route is one entry in the explicit route table. The view-only guarantee is |
| 6 | 11 | // structural: Handler() consults web.mode when building the table, and the |
| @@ -88,5 +93,27 @@ func (s *Server) Handler() http.Handler { | ||
| 88 | 93 | for _, r := range s.Routes() { |
| 89 | 94 | mux.HandleFunc(r.Method+" "+r.Pattern, r.Handler) |
| 90 | 95 | } |
| 91 | return mux | |
| 96 | if len(s.cfg.GoImport) == 0 { | |
| 97 | return mux | |
| 98 | } | |
| 99 | // Vanity Go modules: ?go-get=1 requests under a configured module | |
| 100 | // path answer with the go-import meta tag before normal routing. | |
| 101 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 102 | if r.URL.Query().Get("go-get") == "1" { | |
| 103 | host := r.Host | |
| 104 | if h, _, err := net.SplitHostPort(host); err == nil { | |
| 105 | host = h | |
| 106 | } | |
| 107 | requested := host + strings.TrimSuffix(r.URL.Path, "/") | |
| 108 | for module, repo := range s.cfg.GoImport { | |
| 109 | if requested == module || strings.HasPrefix(requested, module+"/") { | |
| 110 | w.Header().Set("Content-Type", "text/html; charset=utf-8") | |
| 111 | fmt.Fprintf(w, `<!DOCTYPE html><html><head><meta name="go-import" content="%s git %s/%s.git"></head><body>%s</body></html>`, | |
| 112 | module, s.cfg.Server.SiteURL, repo, module) | |
| 113 | return | |
| 114 | } | |
| 115 | } | |
| 116 | } | |
| 117 | mux.ServeHTTP(w, r) | |
| 118 | }) | |
| 92 | 119 | } |
internal/policy/names.go +1
| @@ -16,6 +16,7 @@ var reservedNames = map[string]bool{ | ||
| 16 | 16 | "archive": true, |
| 17 | 17 | "explore": true, |
| 18 | 18 | "favicon.svg": true, |
| 19 | "gitbay": true, // vanity go-import path on gitbay.org | |
| 19 | 20 | "login": true, |
| 20 | 21 | "logout": true, |
| 21 | 22 | "new": true, |