Commit c0d45bd130
Verified · cmc
Layout: unified · split
README.org +6
| @@ -100,6 +100,12 @@ Every read command takes =--json=; stdout is data, stderr is messages; | ||
| 100 | 100 | exit codes are stable (0 ok, 2 usage, 3 not found, 4 denied). Man pages |
| 101 | 101 | via =gitbay man=, completions via =gitbay completion <shell>=. |
| 102 | 102 | |
| 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 | ||
| 103 | 109 | * Development |
| 104 | 110 | |
| 105 | 111 | #+begin_src sh |
docs/admin.org added +140
| @@ -0,0 +1,140 @@ | ||
| 1 | #+title: gitbay admin guide | |
| 2 | ||
| 3 | One static binary (=gitbayd=), one SQLite file, bare repositories on | |
| 4 | disk, and the system =git=. Schema migrations run automatically on | |
| 5 | startup and on every admin command. | |
| 6 | ||
| 7 | * Install | |
| 8 | ||
| 9 | #+begin_src sh | |
| 10 | install -m 755 gitbayd /usr/local/bin/ | |
| 11 | adduser --system --group --home /var/lib/gitbay --shell /usr/sbin/nologin gitbay | |
| 12 | install -d -o gitbay -g gitbay -m 750 /var/lib/gitbay | |
| 13 | gitbayd --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 | |
| 17 | unit, and a nightly backup timer. Run as the unprivileged =gitbay= user; | |
| 18 | the unit's =AmbientCapabilities=CAP_NET_BIND_SERVICE= covers ports | |
| 19 | 22/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 | |
| 39 | contradiction; =--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 | |
| 101 | gitbayd admin user create alice --key alice.pub --email a@example.org --verified [--admin] | |
| 102 | gitbayd admin email verify alice a@example.org # admin assertion, no SMTP needed | |
| 103 | gitbayd 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 | |
| 107 | records which. Verified emails are what make commit signatures | |
| 108 | meaningful — an unverified address never produces a =verified= badge. | |
| 109 | ||
| 110 | * Backup and restore | |
| 111 | ||
| 112 | #+begin_src sh | |
| 113 | gitbayd admin backup --out /var/backups/gitbay/backup.tar.gz | |
| 114 | #+end_src | |
| 115 | ||
| 116 | One archive: a consistent SQLite snapshot (taken *before* the | |
| 117 | repositories are read, so the database never references objects the | |
| 118 | archive missed), every repository, and the SSH host keys. Excluded: | |
| 119 | hook socket, regenerated hook scripts, WAL files. Safe to run against a | |
| 120 | live daemon. | |
| 121 | ||
| 122 | Restore: extract into an empty directory, point =server.root= at it, | |
| 123 | start gitbayd. Host keys are preserved, so clients keep their | |
| 124 | known_hosts entries; hooks regenerate at startup. | |
| 125 | ||
| 126 | * Upgrades | |
| 127 | ||
| 128 | Replace the binary, restart the unit. Migrations apply automatically and | |
| 129 | are transactional; hook scripts under =<root>/hooks= are rewritten at | |
| 130 | startup 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 | ||
| 5 | One endpoint fronts the entire command registry — everything the SSH | |
| 6 | control plane can do, current and future, with identical semantics. | |
| 7 | Disabled by default; the instance must set: | |
| 8 | ||
| 9 | #+begin_src toml | |
| 10 | [api] | |
| 11 | enabled = true | |
| 12 | #+end_src | |
| 13 | ||
| 14 | ** Tokens | |
| 15 | ||
| 16 | Tokens are minted over SSH and only over SSH — an API token can never | |
| 17 | create further credentials. | |
| 18 | ||
| 19 | #+begin_src sh | |
| 20 | gitbay auth token create --name ci [--scope full|read] [--ttl 30d] | |
| 21 | gitbay auth token list | |
| 22 | gitbay auth token revoke ci | |
| 23 | #+end_src | |
| 24 | ||
| 25 | The token (prefix =gb_=, shown exactly once) is presented as | |
| 26 | =Authorization: Bearer gb_...=. Only a hash is stored server-side. | |
| 27 | Scope =read= permits list/show/log/diff-style commands and refuses | |
| 28 | anything that modifies state. =--ttl= takes Go durations or a day | |
| 29 | suffix (=30d=); expired, revoked, and unknown tokens all answer the | |
| 30 | same 401. | |
| 31 | ||
| 32 | ** POST /api/v1/cmd | |
| 33 | ||
| 34 | Request 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 | |
| 41 | command's own JSON envelope with =exit_code= added, plus =stderr= when | |
| 42 | the command wrote diagnostics: | |
| 43 | ||
| 44 | #+begin_src json | |
| 45 | {"protocol_version": 1, "exit_code": 0, "data": {"number": 7}} | |
| 46 | #+end_src | |
| 47 | ||
| 48 | HTTP status maps the exit code: 0→200, 2→400, 3→404, 4→403, else 500. | |
| 49 | Commands that emit raw text rather than an envelope (=help=, =mr diff=) | |
| 50 | come wrapped as ={"output": "..."}=. Git transport commands and the | |
| 51 | token commands are refused by name. | |
| 52 | ||
| 53 | #+begin_src sh | |
| 54 | curl -s -H "Authorization: Bearer $TOKEN" \ | |
| 55 | -d '{"argv":["whoami"]}' https://gitbay.org/api/v1/cmd | |
| 56 | ||
| 57 | printf '{"argv":["issue","create","you/project","--title","t","--file","-"],"stdin":"body\n"}' | | |
| 58 | curl -s -H "Authorization: Bearer $TOKEN" -d @- https://gitbay.org/api/v1/cmd | |
| 59 | #+end_src | |
| 60 | ||
| 61 | * Webhooks | |
| 62 | ||
| 63 | Per-repository outbound POSTs for repository events. Managed by repo | |
| 64 | admins: | |
| 65 | ||
| 66 | #+begin_src sh | |
| 67 | gitbay webhook add <url> --secret s3cret [--events push,issue.created] # default * | |
| 68 | gitbay webhook list | |
| 69 | gitbay webhook deliveries [--limit 50] # status, attempts, last error | |
| 70 | gitbay webhook redeliver <delivery-id> # requeue, including dead letters | |
| 71 | gitbay 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 | ||
| 82 | Each 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 | ||
| 93 | Headers: =X-Gitbay-Event=, =X-Gitbay-Delivery= (id), and — when the hook | |
| 94 | has a secret — =X-Gitbay-Signature-256: sha256=<hex hmac>= over the raw | |
| 95 | body. Verify before trusting: | |
| 96 | ||
| 97 | #+begin_src python | |
| 98 | import hmac, hashlib | |
| 99 | expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest() | |
| 100 | ok = hmac.compare_digest(expected, request.headers["X-Gitbay-Signature-256"]) | |
| 101 | #+end_src | |
| 102 | ||
| 103 | A 2xx within 10 seconds is success. Anything else retries with | |
| 104 | exponential backoff (30s base, doubling) and dead-letters after five | |
| 105 | attempts; =webhook deliveries= shows the trail and =redeliver= revives a | |
| 106 | dead letter. Redirects are never followed, and targets resolving to | |
| 107 | loopback/private/link-local addresses are refused both at registration | |
| 108 | and 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 | ||
| 3 | Everything here works from stock OpenSSH — replace =gitbay= with | |
| 4 | =ssh git@<host>= in any command and it behaves identically. The CLI adds | |
| 5 | convenience (instance profiles, repo inference, =$EDITOR=), nothing more. | |
| 6 | =ssh git@<host> help= lists every command the server knows. | |
| 7 | ||
| 8 | * Getting an account | |
| 9 | ||
| 10 | How 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 | ||
| 32 | Your key is your identity; there are no passwords anywhere. The SSH | |
| 33 | username is always =git= — the key alone determines who you are. | |
| 34 | ||
| 35 | #+begin_src sh | |
| 36 | gitbay auth keys list | |
| 37 | gitbay auth keys add --scope git < ~/.ssh/ci_key.pub # key on stdin | |
| 38 | gitbay auth keys remove SHA256:... | |
| 39 | #+end_src | |
| 40 | ||
| 41 | Scopes: =full= (default; git plus every control command) or =git= (git | |
| 42 | transport only — right for CI and automation keys, which then cannot | |
| 43 | touch issues, settings, or your account). | |
| 44 | ||
| 45 | A key belongs to exactly one account instance-wide. Registering a key | |
| 46 | someone else already holds is refused without telling you whose it is. | |
| 47 | ||
| 48 | * Verified commits | |
| 49 | ||
| 50 | The 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 | |
| 52 | account, and the author email is a verified address on that account. | |
| 53 | ||
| 54 | For OpenPGP signing (git's default): | |
| 55 | #+begin_src sh | |
| 56 | gpg --armor --export you@example.org | gitbay auth pgp add | |
| 57 | #+end_src | |
| 58 | ||
| 59 | For SSH signing (=git config gpg.format ssh=): sign with any key | |
| 60 | registered on your account; your verified addresses act as the principal | |
| 61 | set. No separate registration step. | |
| 62 | ||
| 63 | Add and verify additional addresses with =email add <address>= / | |
| 64 | =email verify <code>= (requires the instance to have SMTP; otherwise an | |
| 65 | admin can assert an address for you). | |
| 66 | ||
| 67 | The states you will see, in decreasing order of trust: =verified=, | |
| 68 | =signed_unknown_key= (valid signature, key not registered here — register | |
| 69 | it and history upgrades retroactively), =signed_email_mismatch= (real | |
| 70 | key, 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 | |
| 73 | signing key on principle. | |
| 74 | ||
| 75 | * Repositories | |
| 76 | ||
| 77 | #+begin_src sh | |
| 78 | gitbay repo create you/project [--private] | |
| 79 | gitbay repo clone you/project | |
| 80 | gitbay repo list | |
| 81 | gitbay repo show you/project | |
| 82 | gitbay repo log you/project --limit 20 # commits with signature states | |
| 83 | gitbay repo fork other/project [--name mine] | |
| 84 | gitbay repo delete you/project --yes | |
| 85 | #+end_src | |
| 86 | ||
| 87 | Pushing is SSH-only. Public repositories are anonymously readable over | |
| 88 | HTTPS (and =git://= where enabled); private repositories exist only over | |
| 89 | SSH and answer "not found" to everyone without access. | |
| 90 | ||
| 91 | Access and settings (owner or =admin= grant): | |
| 92 | #+begin_src sh | |
| 93 | gitbay repo access grant you/project alice write # read | write | admin | |
| 94 | gitbay repo access revoke you/project alice | |
| 95 | gitbay repo settings protect you/project main # no force-push, no delete | |
| 96 | gitbay repo settings require-signed you/project on # every commit must verify | |
| 97 | gitbay repo settings git-daemon you/project on # expose over git:// | |
| 98 | #+end_src | |
| 99 | ||
| 100 | Import from another forge (git data only — issues and PRs do not | |
| 101 | transfer): | |
| 102 | #+begin_src sh | |
| 103 | gitbay 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 | ||
| 109 | Orgs share the owner namespace with users and own repositories at | |
| 110 | =org/repo=. Members get write on all org repos; org admins get repo | |
| 111 | admin, create repos under the org, and manage membership. | |
| 112 | ||
| 113 | #+begin_src sh | |
| 114 | gitbay org create krz | |
| 115 | gitbay org members add krz alice [--role admin] | |
| 116 | gitbay org show krz | |
| 117 | gitbay org rename krz newname # clone URLs change | |
| 118 | gitbay org delete krz --yes # only when it owns no repositories | |
| 119 | #+end_src | |
| 120 | ||
| 121 | * Issues | |
| 122 | ||
| 123 | Anyone who can read a repository can file and comment. Closing/reopening | |
| 124 | is for the author or anyone with write; labels and assignees need write. | |
| 125 | ||
| 126 | #+begin_src sh | |
| 127 | gitbay issue create --title "it breaks" [--body "..." | --file -] | |
| 128 | gitbay issue list [--state open|closed|all] | |
| 129 | gitbay issue show 4 | |
| 130 | gitbay issue comment 4 --message "same here" | |
| 131 | gitbay issue close 4 / reopen 4 | |
| 132 | gitbay issue label 4 --add bug --remove wontfix | |
| 133 | gitbay issue assign 4 --add alice | |
| 134 | #+end_src | |
| 135 | ||
| 136 | Inside a clone, the repository is inferred from the =origin= remote — | |
| 137 | that is why no =owner/name= appears above. Anywhere else, pass it as the | |
| 138 | first argument. Long text: =--body= inline, =--file -= from stdin, or | |
| 139 | neither on a terminal and =$EDITOR= opens. | |
| 140 | ||
| 141 | * Merge requests | |
| 142 | ||
| 143 | #+begin_src sh | |
| 144 | gitbay mr create --source feature --target main --title "add thing" | |
| 145 | gitbay mr create other/upstream --source you/fork:feature --target main --title "..." | |
| 146 | gitbay mr list / show 4 / diff 4 | |
| 147 | gitbay mr checkout 4 # local branch mr/4 from the MR head | |
| 148 | gitbay mr review 4 --approve # or --request-changes / --comment | |
| 149 | gitbay mr merge 4 [--strategy ff|merge|squash|rebase] | |
| 150 | gitbay mr close 4 | |
| 151 | #+end_src | |
| 152 | ||
| 153 | Semantics 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 | ||
| 171 | Every read command takes =--json= and emits one envelope: | |
| 172 | ={"protocol_version": 1, "data": ...}=. stdout is data, stderr is | |
| 173 | messages. Exit codes are stable: 0 ok, 1 failure, 2 usage, 3 not found, | |
| 174 | 4 denied, 5 server/protocol error. Nothing ever prompts; destructive | |
| 175 | commands take =--yes=. | |
| 176 | ||
| 177 | For HTTP automation see =docs/api.org=. | |
| 178 | ||
| 179 | * CLI setup | |
| 180 | ||
| 181 | #+begin_src sh | |
| 182 | gitbay remote add myforge forge.example.org [--port n] [--user u] --default | |
| 183 | gitbay remote list | |
| 184 | gitbay init [name] [--private] # git init + repo create + origin, in one step | |
| 185 | #+end_src | |
| 186 | ||
| 187 | Configuration lives at =~/.config/gitbay/config.toml=. The CLI shells | |
| 188 | out to your real =ssh=, so =~/.ssh/config=, the agent, and hardware keys | |
| 189 | all apply. Man pages: =gitbay man --dir <dir>=; completions: | |
| 190 | =gitbay completion bash|zsh|fish=. | |