Commit c0d45bd130

c0d45bd13028b5cad90bac2dd1efad6390908e0d

parent: db5915fa14

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-24 01:45 UTC

docs: user guide, admin guide, API/webhook reference

Layout: unified · split

README.org +6
@@ -100,6 +100,12 @@ Every read command takes =--json=; stdout is data, stderr is messages;
100100exit codes are stable (0 ok, 2 usage, 3 not found, 4 denied). Man pages
101101via =gitbay man=, completions via =gitbay completion <shell>=.
102102
103* Documentation
104
105- [[file:docs/users.org][user guide]] — accounts, keys, verified commits, repos, issues, MRs, scripting
106- [[file:docs/admin.org][admin guide]] — install, full configuration reference, backup/restore, upgrades
107- [[file:docs/api.org][API and webhooks]] — the JSON API contract, tokens, webhook payloads and HMAC
108
103109* Development
104110
105111#+begin_src sh
docs/admin.org added +140
@@ -0,0 +1,140 @@
1#+title: gitbay admin guide
2
3One static binary (=gitbayd=), one SQLite file, bare repositories on
4disk, and the system =git=. Schema migrations run automatically on
5startup and on every admin command.
6
7* Install
8
9#+begin_src sh
10install -m 755 gitbayd /usr/local/bin/
11adduser --system --group --home /var/lib/gitbay --shell /usr/sbin/nologin gitbay
12install -d -o gitbay -g gitbay -m 750 /var/lib/gitbay
13gitbayd --config /etc/gitbay/config.toml check-config
14#+end_src
15
16=deploy/= in the source tree has a cloud-init file, a hardened systemd
17unit, and a nightly backup timer. Run as the unprivileged =gitbay= user;
18the unit's =AmbientCapabilities=CAP_NET_BIND_SERVICE= covers ports
1922/80/443 without root.
20
21** The SSH port decision
22
23- =ssh.mode = "embedded"= (default): gitbayd itself listens, normally on
24 22 — move the host's admin sshd to another port. Remotes read
25 =git@host:owner/repo= with no port gymnastics.
26- =ssh.mode = "system"=: the host sshd owns 22 and invokes gitbayd via
27 =AuthorizedKeysCommand=:
28 #+begin_example
29 AuthorizedKeysCommand /usr/local/bin/gitbayd --config /etc/gitbay/config.toml authorized-keys %t %k
30 AuthorizedKeysCommandUser gitbay
31 #+end_example
32 sshd requires that binary to be root-owned and not group/world
33 writable. Unknown keys fail authentication inside sshd, so system mode
34 requires =registration.mode = "closed"= (check-config enforces this).
35
36* Configuration reference
37
38=/etc/gitbay/config.toml=. =check-config= validates and names every
39contradiction; =--no-host-checks= skips port/path probes.
40
41** [server]
42- =root= (default =/var/lib/gitbay=) — repositories, database, host
43 keys, ACME cache all live here.
44- =site_url= (required) — canonical =https://host=; drives ACME, clone
45 URLs, mail links.
46
47** [ssh]
48- =mode= — =embedded= | =system= (above).
49- =port= (22) — embedded listener port.
50- =host_keys= — list of private key paths; empty generates an ed25519
51 key at =<root>/ssh/host_ed25519=.
52
53** [http]
54- =addr= (=:443=), =tls= — =acme= | =files= | =off=.
55- =acme=: certificates via TLS-ALPN-01 on the HTTPS port, cached at
56 =<root>/acme=; =acme_email= for the CA account; =acme_http_addr=
57 (=:80=, ="off"= to disable) adds HTTP-01 and an https redirect —
58 failing to bind it is a warning, not fatal. Requires an =https://=
59 site_url with a public DNS name.
60- =files=: =cert_file= + =key_file=.
61- =off=: plain HTTP — development, or behind a TLS-terminating proxy.
62
63** [web]
64- =mode= — =view_only= (default) | =accounts=. In view_only the mutating
65 web routes are never registered; in accounts, browser sessions are
66 minted over SSH (=web login=), and users with write access can create
67 repos, comment, and make simple file edits (which commit unsigned,
68 honestly). =password_auth= is reserved and currently rejected.
69
70** [registration]
71- =mode= — =closed= (default) | =invite= | =open=. invite/open require
72 [mail]. See the user guide for the flows.
73
74** [mail]
75- =smtp_host= (host:port, 587 assumed), =from=, optional =smtp_user= /
76 =smtp_pass=. STARTTLS when offered. Required for invite/open
77 registration and self-service =email add=; in closed mode you may omit
78 it entirely and assert addresses by hand (below).
79
80** [api]
81- =enabled= (false) — the JSON API surface; see =docs/api.org=. Off
82 means no credential-bearing HTTP endpoint exists at all.
83
84** [webhooks]
85- =allow_local= (false) — permit webhook targets on loopback/private
86 addresses. Leave off unless you know why you need it (SSRF).
87
88** [limits]
89- =clone_timeout= (3600s) — cap on =repo import= fetches.
90- =max_blob_bytes= (100MB) — cap on raw file serving over the web.
91- =max_pack_bytes=, =ssh_auth_rate= — reserved, not yet enforced.
92
93** [git_daemon]
94- =enabled= (false), =port= (9418) — the anonymous =git://= listener.
95 Serves only public repositories that additionally ran
96 =repo settings git-daemon <repo> on=.
97
98* Users, email, invites
99
100#+begin_src sh
101gitbayd admin user create alice --key alice.pub --email a@example.org --verified [--admin]
102gitbayd admin email verify alice a@example.org # admin assertion, no SMTP needed
103gitbayd admin invite --email b@example.org # mails a code; prints it if no SMTP
104#+end_src
105
106"Verified" means SMTP-confirmed or host-admin-asserted; the database
107records which. Verified emails are what make commit signatures
108meaningful — an unverified address never produces a =verified= badge.
109
110* Backup and restore
111
112#+begin_src sh
113gitbayd admin backup --out /var/backups/gitbay/backup.tar.gz
114#+end_src
115
116One archive: a consistent SQLite snapshot (taken *before* the
117repositories are read, so the database never references objects the
118archive missed), every repository, and the SSH host keys. Excluded:
119hook socket, regenerated hook scripts, WAL files. Safe to run against a
120live daemon.
121
122Restore: extract into an empty directory, point =server.root= at it,
123start gitbayd. Host keys are preserved, so clients keep their
124known_hosts entries; hooks regenerate at startup.
125
126* Upgrades
127
128Replace the binary, restart the unit. Migrations apply automatically and
129are transactional; hook scripts under =<root>/hooks= are rewritten at
130startup to point at the current binary path.
131
132* Odds and ends
133
134- deleting a fork marks MRs sourced from it =source_gone=; their diffs
135 remain viewable and mergeable because the target repo owns the
136 objects.
137- =refs/merge-requests/*= is server-owned and unpushable by clients.
138- audit-relevant activity (issue/MR lifecycle, imports, pushes) lands in
139 the =events= table, which also feeds webhooks.
140- the daemon idles under 10MB RSS; the smallest VPS tier is adequate.
docs/api.org added +109
@@ -0,0 +1,109 @@
1#+title: gitbay API and webhooks
2
3* The JSON API
4
5One endpoint fronts the entire command registry — everything the SSH
6control plane can do, current and future, with identical semantics.
7Disabled by default; the instance must set:
8
9#+begin_src toml
10[api]
11enabled = true
12#+end_src
13
14** Tokens
15
16Tokens are minted over SSH and only over SSH — an API token can never
17create further credentials.
18
19#+begin_src sh
20gitbay auth token create --name ci [--scope full|read] [--ttl 30d]
21gitbay auth token list
22gitbay auth token revoke ci
23#+end_src
24
25The token (prefix =gb_=, shown exactly once) is presented as
26=Authorization: Bearer gb_...=. Only a hash is stored server-side.
27Scope =read= permits list/show/log/diff-style commands and refuses
28anything that modifies state. =--ttl= takes Go durations or a day
29suffix (=30d=); expired, revoked, and unknown tokens all answer the
30same 401.
31
32** POST /api/v1/cmd
33
34Request body:
35#+begin_src json
36{"argv": ["issue", "create", "you/project", "--title", "from CI"],
37 "stdin": "optional body for --file - style input"}
38#+end_src
39
40=argv= is real argv — no shell, no quoting rules. The response is the
41command's own JSON envelope with =exit_code= added, plus =stderr= when
42the command wrote diagnostics:
43
44#+begin_src json
45{"protocol_version": 1, "exit_code": 0, "data": {"number": 7}}
46#+end_src
47
48HTTP status maps the exit code: 0→200, 2→400, 3→404, 4→403, else 500.
49Commands that emit raw text rather than an envelope (=help=, =mr diff=)
50come wrapped as ={"output": "..."}=. Git transport commands and the
51token commands are refused by name.
52
53#+begin_src sh
54curl -s -H "Authorization: Bearer $TOKEN" \
55 -d '{"argv":["whoami"]}' https://gitbay.org/api/v1/cmd
56
57printf '{"argv":["issue","create","you/project","--title","t","--file","-"],"stdin":"body\n"}' |
58curl -s -H "Authorization: Bearer $TOKEN" -d @- https://gitbay.org/api/v1/cmd
59#+end_src
60
61* Webhooks
62
63Per-repository outbound POSTs for repository events. Managed by repo
64admins:
65
66#+begin_src sh
67gitbay webhook add <url> --secret s3cret [--events push,issue.created] # default *
68gitbay webhook list
69gitbay webhook deliveries [--limit 50] # status, attempts, last error
70gitbay webhook redeliver <delivery-id> # requeue, including dead letters
71gitbay webhook remove <id>
72#+end_src
73
74** Events
75
76=push= (data: ref, old, new, forced, deleted), =issue.created=,
77=issue.commented=, =issue.closed=, =issue.open=, =mr.created=,
78=mr.merged= (data: number, sha), =repo.imported= (data: from).
79
80** Delivery
81
82Each event POSTs one JSON body:
83
84#+begin_src json
85{"event": "push",
86 "repo": "you/project",
87 "actor": "alice",
88 "created_at": "2026-08-24T01:00:00.000Z",
89 "data": {"ref": "refs/heads/main", "old": "...", "new": "...",
90 "forced": false, "deleted": false}}
91#+end_src
92
93Headers: =X-Gitbay-Event=, =X-Gitbay-Delivery= (id), and — when the hook
94has a secret — =X-Gitbay-Signature-256: sha256=<hex hmac>= over the raw
95body. Verify before trusting:
96
97#+begin_src python
98import hmac, hashlib
99expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
100ok = hmac.compare_digest(expected, request.headers["X-Gitbay-Signature-256"])
101#+end_src
102
103A 2xx within 10 seconds is success. Anything else retries with
104exponential backoff (30s base, doubling) and dead-letters after five
105attempts; =webhook deliveries= shows the trail and =redeliver= revives a
106dead letter. Redirects are never followed, and targets resolving to
107loopback/private/link-local addresses are refused both at registration
108and again at connect time, unless the instance sets
109=[webhooks] allow_local=.
docs/users.org added +190
@@ -0,0 +1,190 @@
1#+title: gitbay user guide
2
3Everything here works from stock OpenSSH — replace =gitbay= with
4=ssh git@<host>= in any command and it behaves identically. The CLI adds
5convenience (instance profiles, repo inference, =$EDITOR=), nothing more.
6=ssh git@<host> help= lists every command the server knows.
7
8* Getting an account
9
10How you join depends on the instance's registration mode:
11
12- closed :: an admin creates your account on the host and registers your
13 first SSH key. Nothing for you to do but hand over your public key.
14- invite :: you receive a single-use code by email. With the SSH key you
15 want to use:
16 #+begin_src sh
17 ssh git@<host> register --username you --invite <code>
18 #+end_src
19 Your account is active immediately; the invited address is your
20 verified email.
21- open ::
22 #+begin_src sh
23 ssh git@<host> register --username you --email you@example.org
24 #+end_src
25 A verification code arrives by mail. Until you run
26 =ssh git@<host> email verify <code>=, the account is pending: you can
27 run =whoami= and the email commands, and nothing else — no git, no
28 repos.
29
30* SSH keys
31
32Your key is your identity; there are no passwords anywhere. The SSH
33username is always =git= — the key alone determines who you are.
34
35#+begin_src sh
36gitbay auth keys list
37gitbay auth keys add --scope git < ~/.ssh/ci_key.pub # key on stdin
38gitbay auth keys remove SHA256:...
39#+end_src
40
41Scopes: =full= (default; git plus every control command) or =git= (git
42transport only — right for CI and automation keys, which then cannot
43touch issues, settings, or your account).
44
45A key belongs to exactly one account instance-wide. Registering a key
46someone else already holds is refused without telling you whose it is.
47
48* Verified commits
49
50The commit badge is driven by the *author* email and the signing key:
51=verified= means the signature is valid, the key is registered to an
52account, and the author email is a verified address on that account.
53
54For OpenPGP signing (git's default):
55#+begin_src sh
56gpg --armor --export you@example.org | gitbay auth pgp add
57#+end_src
58
59For SSH signing (=git config gpg.format ssh=): sign with any key
60registered on your account; your verified addresses act as the principal
61set. No separate registration step.
62
63Add and verify additional addresses with =email add <address>= /
64=email verify <code>= (requires the instance to have SMTP; otherwise an
65admin can assert an address for you).
66
67The states you will see, in decreasing order of trust: =verified=,
68=signed_unknown_key= (valid signature, key not registered here — register
69it and history upgrades retroactively), =signed_email_mismatch= (real
70key, author line claims someone else), =signed_key_expired= /
71=signed_key_revoked=, =bad_signature=, =unsigned=. Server-created commits
72(web edits, merge commits) are always =unsigned= — the server holds no
73signing key on principle.
74
75* Repositories
76
77#+begin_src sh
78gitbay repo create you/project [--private]
79gitbay repo clone you/project
80gitbay repo list
81gitbay repo show you/project
82gitbay repo log you/project --limit 20 # commits with signature states
83gitbay repo fork other/project [--name mine]
84gitbay repo delete you/project --yes
85#+end_src
86
87Pushing is SSH-only. Public repositories are anonymously readable over
88HTTPS (and =git://= where enabled); private repositories exist only over
89SSH and answer "not found" to everyone without access.
90
91Access and settings (owner or =admin= grant):
92#+begin_src sh
93gitbay repo access grant you/project alice write # read | write | admin
94gitbay repo access revoke you/project alice
95gitbay repo settings protect you/project main # no force-push, no delete
96gitbay repo settings require-signed you/project on # every commit must verify
97gitbay repo settings git-daemon you/project on # expose over git://
98#+end_src
99
100Import from another forge (git data only — issues and PRs do not
101transfer):
102#+begin_src sh
103gitbay repo import you/mirror --from https://github.com/you/repo.git \
104 [--private] [--token-stdin] # token on stdin, never in the URL
105#+end_src
106
107* Organizations
108
109Orgs share the owner namespace with users and own repositories at
110=org/repo=. Members get write on all org repos; org admins get repo
111admin, create repos under the org, and manage membership.
112
113#+begin_src sh
114gitbay org create krz
115gitbay org members add krz alice [--role admin]
116gitbay org show krz
117gitbay org rename krz newname # clone URLs change
118gitbay org delete krz --yes # only when it owns no repositories
119#+end_src
120
121* Issues
122
123Anyone who can read a repository can file and comment. Closing/reopening
124is for the author or anyone with write; labels and assignees need write.
125
126#+begin_src sh
127gitbay issue create --title "it breaks" [--body "..." | --file -]
128gitbay issue list [--state open|closed|all]
129gitbay issue show 4
130gitbay issue comment 4 --message "same here"
131gitbay issue close 4 / reopen 4
132gitbay issue label 4 --add bug --remove wontfix
133gitbay issue assign 4 --add alice
134#+end_src
135
136Inside a clone, the repository is inferred from the =origin= remote —
137that is why no =owner/name= appears above. Anywhere else, pass it as the
138first argument. Long text: =--body= inline, =--file -= from stdin, or
139neither on a terminal and =$EDITOR= opens.
140
141* Merge requests
142
143#+begin_src sh
144gitbay mr create --source feature --target main --title "add thing"
145gitbay mr create other/upstream --source you/fork:feature --target main --title "..."
146gitbay mr list / show 4 / diff 4
147gitbay mr checkout 4 # local branch mr/4 from the MR head
148gitbay mr review 4 --approve # or --request-changes / --comment
149gitbay mr merge 4 [--strategy ff|merge|squash|rebase]
150gitbay mr close 4
151#+end_src
152
153Semantics worth knowing:
154
155- the MR head lives in the *target* repository as
156 =refs/merge-requests/N/head= (fetchable by any reader), so an MR
157 survives deletion of its source branch or fork.
158- force-pushing the source updates the MR and marks existing reviews
159 stale.
160- default strategy: fast-forward when possible, else a merge commit.
161 Squash makes one commit authored by the MR author, committed by the
162 merger. Rebase replays a linear range preserving authors; it refuses
163 ranges containing merge commits, and when fast-forward is possible it
164 *is* one (original commits and signatures land untouched).
165- on =require_signed_commits= branches only fast-forwards of fully
166 verified commits merge; everything server-created is refused with
167 instructions to rebase locally.
168
169* Scripting
170
171Every read command takes =--json= and emits one envelope:
172={"protocol_version": 1, "data": ...}=. stdout is data, stderr is
173messages. Exit codes are stable: 0 ok, 1 failure, 2 usage, 3 not found,
1744 denied, 5 server/protocol error. Nothing ever prompts; destructive
175commands take =--yes=.
176
177For HTTP automation see =docs/api.org=.
178
179* CLI setup
180
181#+begin_src sh
182gitbay remote add myforge forge.example.org [--port n] [--user u] --default
183gitbay remote list
184gitbay init [name] [--private] # git init + repo create + origin, in one step
185#+end_src
186
187Configuration lives at =~/.config/gitbay/config.toml=. The CLI shells
188out to your real =ssh=, so =~/.ssh/config=, the agent, and hardware keys
189all apply. Man pages: =gitbay man --dir <dir>=; completions:
190=gitbay completion bash|zsh|fish=.