Commit 4254359d23
Verified · cmc
CONTRIBUTING.org +2 −2
| @@ -37,7 +37,7 @@ ssh git@gitbay.org register --username you --invite <code> | ||
| 37 | 37 | |
| 38 | 38 | * What holds the design together |
| 39 | 39 | |
| 40 | Read =docs/roadmap.org= for direction. The invariants that reviews will | |
| 40 | Read the [[https://gitbay.org/krz/gitbay/wiki/Roadmap][roadmap]] (in the wiki) for direction. The invariants that reviews will | |
| 41 | 41 | hold you to: |
| 42 | 42 | |
| 43 | 43 | - every control command must work from bare OpenSSH; the registry test |
| @@ -59,7 +59,7 @@ surrounding code; keep diffs surgical. | ||
| 59 | 59 | * Issues |
| 60 | 60 | |
| 61 | 61 | File at [[https://gitbay.org/krz/gitbay/issues][the tracker]] (=gitbay issue create= from a clone). Check |
| 62 | =docs/roadmap.org= first — it maps the filed issues to phases. | |
| 62 | the [[https://gitbay.org/krz/gitbay/wiki/Roadmap][roadmap]] (in the wiki) first — it maps the filed issues to phases. | |
| 63 | 63 | |
| 64 | 64 | * License |
| 65 | 65 | |
README.org +9 −4
| @@ -102,10 +102,15 @@ via =gitbay man=, completions via =gitbay completion <shell>=. | ||
| 102 | 102 | |
| 103 | 103 | * Documentation |
| 104 | 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 | - [[file:docs/roadmap.org][roadmap]] — status, phased plan, and what is deliberately not planned | |
| 105 | Docs live in [[https://gitbay.org/krz/gitbay/wiki][the wiki]] — itself a git repository | |
| 106 | (=git clone ssh://git@gitbay.org/krz/gitbay.wiki.git=), dogfooding the | |
| 107 | wiki feature: | |
| 108 | ||
| 109 | [[https://gitbay.org/krz/gitbay/wiki/Users][user guide]] — accounts, keys, verified commits, repos, issues, MRs, scripting | |
| 110 | [[https://gitbay.org/krz/gitbay/wiki/Admin][admin guide]] — install, full configuration reference, backup/restore, security | |
| 111 | [[https://gitbay.org/krz/gitbay/wiki/API][API and webhooks]] — the JSON API contract, tokens, webhook payloads and HMAC | |
| 112 | [[https://gitbay.org/krz/gitbay/wiki/Roadmap][roadmap]] — status, phased plan, and what is deliberately not planned | |
| 113 | [[https://gitbay.org/krz/gitbay/wiki/Threat-Model][threat model]] — what the forge trusts and never does | |
| 109 | 114 | |
| 110 | 115 | * Contributing |
| 111 | 116 | |
docs/admin.org deleted −225
| @@ -1,225 +0,0 @@ | ||
| 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 | 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 | ||
| 16 | #+begin_src sh | |
| 17 | install -m 755 gitbayd /usr/local/bin/ | |
| 18 | adduser --system --group --home /var/lib/gitbay --shell /usr/sbin/nologin gitbay | |
| 19 | install -d -o gitbay -g gitbay -m 750 /var/lib/gitbay | |
| 20 | gitbayd --config /etc/gitbay/config.toml check-config | |
| 21 | #+end_src | |
| 22 | ||
| 23 | =deploy/= in the source tree has a cloud-init file, a hardened systemd | |
| 24 | unit, and a nightly backup timer. Run as the unprivileged =gitbay= user; | |
| 25 | the unit's =AmbientCapabilities=CAP_NET_BIND_SERVICE= covers ports | |
| 26 | 22/80/443 without root. | |
| 27 | ||
| 28 | ** The SSH port decision | |
| 29 | ||
| 30 | - =ssh.mode = "embedded"= (default): gitbayd itself listens, normally on | |
| 31 | 22 — move the host's admin sshd to another port. Remotes read | |
| 32 | =git@host:owner/repo= with no port gymnastics. | |
| 33 | - =ssh.mode = "system"=: the host sshd owns 22 and invokes gitbayd via | |
| 34 | =AuthorizedKeysCommand=: | |
| 35 | #+begin_example | |
| 36 | AuthorizedKeysCommand /usr/local/bin/gitbayd --config /etc/gitbay/config.toml authorized-keys %t %k | |
| 37 | AuthorizedKeysCommandUser gitbay | |
| 38 | #+end_example | |
| 39 | sshd requires that binary to be root-owned and not group/world | |
| 40 | writable. Unknown keys fail authentication inside sshd, so system mode | |
| 41 | requires =registration.mode = "closed"= (check-config enforces this). | |
| 42 | ||
| 43 | * Configuration reference | |
| 44 | ||
| 45 | =/etc/gitbay/config.toml=. =check-config= validates and names every | |
| 46 | contradiction; =--no-host-checks= skips port/path probes. | |
| 47 | ||
| 48 | ** [server] | |
| 49 | - =root= (default =/var/lib/gitbay=) — repositories, database, host | |
| 50 | keys, ACME cache all live here. | |
| 51 | - =site_url= (required) — canonical =https://host=; drives ACME, clone | |
| 52 | URLs, mail links. | |
| 53 | ||
| 54 | ** [ssh] | |
| 55 | - =mode= — =embedded= | =system= (above). | |
| 56 | - =port= (22) — embedded listener port. | |
| 57 | - =host_keys= — list of private key paths; empty generates an ed25519 | |
| 58 | key at =<root>/ssh/host_ed25519=. | |
| 59 | ||
| 60 | ** [http] | |
| 61 | - =addr= (=:443=), =tls= — =acme= | =files= | =off=. | |
| 62 | - =acme=: certificates via TLS-ALPN-01 on the HTTPS port, cached at | |
| 63 | =<root>/acme=; =acme_email= for the CA account; =acme_http_addr= | |
| 64 | (=:80=, ="off"= to disable) adds HTTP-01 and an https redirect — | |
| 65 | failing to bind it is a warning, not fatal. Requires an =https://= | |
| 66 | site_url with a public DNS name. | |
| 67 | - =files=: =cert_file= + =key_file=. | |
| 68 | - =off=: plain HTTP — development, or behind a TLS-terminating proxy. | |
| 69 | ||
| 70 | ** [web] | |
| 71 | - =mode= — =view_only= (default) | =accounts=. In view_only the mutating | |
| 72 | web routes are never registered; in accounts, browser sessions are | |
| 73 | minted over SSH (=web login=), and users with write access can create | |
| 74 | repos, comment, and make simple file edits (which commit unsigned, | |
| 75 | honestly). =password_auth= is reserved and currently rejected. | |
| 76 | ||
| 77 | ** [registration] | |
| 78 | - =mode= — =closed= (default) | =invite= | =open=. invite/open require | |
| 79 | [mail]. See the user guide for the flows. | |
| 80 | ||
| 81 | ** [mail] | |
| 82 | - =smtp_host= (host:port, 587 assumed), =from=, optional =smtp_user= / | |
| 83 | =smtp_pass=. STARTTLS when offered. Required for invite/open | |
| 84 | registration and self-service =email add=; in closed mode you may omit | |
| 85 | it entirely and assert addresses by hand (below). | |
| 86 | ||
| 87 | ** [api] | |
| 88 | - =enabled= (false) — the JSON API surface; see =docs/api.org=. Off | |
| 89 | means no credential-bearing HTTP endpoint exists at all. | |
| 90 | ||
| 91 | ** [webhooks] | |
| 92 | - =allow_local= (false) — permit webhook targets on loopback/private | |
| 93 | addresses. Leave off unless you know why you need it (SSRF). | |
| 94 | ||
| 95 | ** [limits] | |
| 96 | - =clone_timeout= (3600s) — cap on =repo import= fetches. | |
| 97 | - =max_blob_bytes= (100MB) — cap on raw file serving over the web. | |
| 98 | - =max_asset_bytes= (512MB) — cap per uploaded release asset. | |
| 99 | - =max_pack_bytes=, =ssh_auth_rate= — reserved, not yet enforced. | |
| 100 | ||
| 101 | ** [git_daemon] | |
| 102 | - =enabled= (false), =port= (9418) — the anonymous =git://= listener. | |
| 103 | Serves only public repositories that additionally ran | |
| 104 | =repo settings git-daemon <repo> on=. | |
| 105 | ||
| 106 | ** [mirrors] | |
| 107 | - =pull_interval_minutes= (15) — how often pull mirrors fetch their | |
| 108 | upstream. Push mirrors sync shortly after each local ref update. | |
| 109 | Mirror URLs pass the same SSRF rules as webhook targets. | |
| 110 | ||
| 111 | ** [go_import] | |
| 112 | Vanity Go module paths, one per line: ="host/module" = "owner/repo"=. | |
| 113 | Requests with =?go-get=1= at or under the module path answer with the | |
| 114 | go-import meta tag pointing at the repository's HTTPS clone URL, so | |
| 115 | =go install host/module/cmd/...@latest= resolves. The repository should | |
| 116 | be public (the module path itself confirms it exists). | |
| 117 | ||
| 118 | * Users, email, invites | |
| 119 | ||
| 120 | #+begin_src sh | |
| 121 | gitbayd admin user create alice --key alice.pub --email a@example.org --verified [--admin] | |
| 122 | gitbayd admin email verify alice a@example.org # admin assertion, no SMTP needed | |
| 123 | gitbayd admin invite --email b@example.org # mails a code; prints it if no SMTP | |
| 124 | #+end_src | |
| 125 | ||
| 126 | "Verified" means SMTP-confirmed or host-admin-asserted; the database | |
| 127 | records which. Verified emails are what make commit signatures | |
| 128 | meaningful — an unverified address never produces a =verified= badge. | |
| 129 | ||
| 130 | * Audit and account control | |
| 131 | ||
| 132 | The audit log is the security feed (events are the product feed): every | |
| 133 | successful mutating command with its argv and source credential (SSH key | |
| 134 | fingerprint or API), registrations, admin actions, force-pushes, and | |
| 135 | auth failures/throttling. Secrets never appear — they travel on stdin, | |
| 136 | never in argv. | |
| 137 | ||
| 138 | #+begin_src sh | |
| 139 | gitbayd admin audit [--limit n] # host-local | |
| 140 | ssh git@<host> audit [--limit n] # instance admins, SSH only | |
| 141 | gitbayd admin user disable <name> # suspend: SSH, web, API all refused; | |
| 142 | gitbayd admin user enable <name> # sessions dropped, nothing deleted | |
| 143 | #+end_src | |
| 144 | ||
| 145 | =limits.ssh_auth_rate= (10) throttles per-IP authentication *failures* | |
| 146 | per minute — successful auths never count and clear the slate. | |
| 147 | =limits.max_pack_bytes= is enforced as =receive.maxInputSize= on every | |
| 148 | push. | |
| 149 | ||
| 150 | * Maintenance | |
| 151 | ||
| 152 | #+begin_src sh | |
| 153 | gitbayd admin stats [--json] # counts, database size, per-repo disk | |
| 154 | gitbayd admin gc [--repo owner/name] # git gc: repack and prune; per-repo sizes | |
| 155 | gitbayd admin gc --aggressive # thorough repack; slow, rarely needed | |
| 156 | #+end_src | |
| 157 | ||
| 158 | =deploy/cloud-init.yaml= ships a =gitbay-gc.timer= that runs =admin gc= | |
| 159 | weekly (Sunday 07:00 UTC). Imported repositories keep whatever pack | |
| 160 | layout the source sent, so a first manual =admin gc= after a bulk | |
| 161 | import is worthwhile. | |
| 162 | ||
| 163 | * Backup and restore | |
| 164 | ||
| 165 | #+begin_src sh | |
| 166 | gitbayd admin backup --out /var/backups/gitbay/backup.tar.gz | |
| 167 | #+end_src | |
| 168 | ||
| 169 | One archive: a consistent SQLite snapshot (taken *before* the | |
| 170 | repositories are read, so the database never references objects the | |
| 171 | archive missed), every repository, and the SSH host keys. Excluded: | |
| 172 | hook socket, regenerated hook scripts, WAL files. Safe to run against a | |
| 173 | live daemon. | |
| 174 | ||
| 175 | Restore: extract into an empty directory, point =server.root= at it, | |
| 176 | start gitbayd. Host keys are preserved, so clients keep their | |
| 177 | known_hosts entries; hooks regenerate at startup. | |
| 178 | ||
| 179 | * Upgrades | |
| 180 | ||
| 181 | Replace the binary, restart the unit. Migrations apply automatically and | |
| 182 | are transactional; hook scripts under =<root>/hooks= are rewritten at | |
| 183 | startup to point at the current binary path. | |
| 184 | ||
| 185 | * Security | |
| 186 | ||
| 187 | The =docs/threat-model.org= file is the reference for what the forge | |
| 188 | trusts and refuses to do. Operational checklist: | |
| 189 | ||
| 190 | - *Software checks.* =deploy/audit.sh= runs =go vet=, =govulncheck= | |
| 191 | (the module list is deliberately short — review it on each release), | |
| 192 | and a short fuzz pass over every attacker-facing parser (pkt-line, | |
| 193 | commit, SSHSIG armor, OpenPGP key, SSH tokenizer). Run it before | |
| 194 | tagging a release. | |
| 195 | - *Web responses* carry a scripts-forbidden CSP, =X-Frame-Options: | |
| 196 | DENY=, =nosniff=, =no-referrer=, and HSTS when TLS is on — no | |
| 197 | configuration needed. | |
| 198 | - *Host sandboxing.* The systemd unit in =deploy/cloud-init.yaml= runs | |
| 199 | gitbayd unprivileged with =ProtectSystem=strict=, =PrivateDevices=, | |
| 200 | =LockPersonality=, =MemoryDenyWriteExecute=, | |
| 201 | =SystemCallFilter=@system-service=, and =RestrictAddressFamilies= to | |
| 202 | INET/INET6/UNIX. It keeps =CAP_NET_BIND_SERVICE= only, to bind 22/80/443. | |
| 203 | - *OS patches* apply via =unattended-upgrades= (security origins, | |
| 204 | auto-reboot 04:30 if required). | |
| 205 | - *Admin sshd (2222)* is throttled by =MaxStartups=/=MaxAuthTries= and | |
| 206 | watched by =fail2ban=; gitbayd's own port 22 is throttled by | |
| 207 | =limits.ssh_auth_rate= (auth failures per IP per minute). | |
| 208 | - *Monitoring.* =gitbay-monitor.timer= posts disk/service/cert status | |
| 209 | hourly to the webhook URL in =/etc/gitbay/monitor.url= (create the | |
| 210 | file to enable; absent = silent). Alerts fire on a stopped service or | |
| 211 | disk ≥ 85%. | |
| 212 | - *Database.* =gitbay.db= and its WAL live under =/var/lib/gitbay= (mode | |
| 213 | 0750, owned by =gitbay=). The nightly archive plus provider snapshots | |
| 214 | are the recovery path; for tighter RPO, add continuous replication | |
| 215 | (litestream) against the same file — it coexists with the WAL. | |
| 216 | ||
| 217 | * Odds and ends | |
| 218 | ||
| 219 | - deleting a fork marks MRs sourced from it =source_gone=; their diffs | |
| 220 | remain viewable and mergeable because the target repo owns the | |
| 221 | objects. | |
| 222 | - =refs/merge-requests/*= is server-owned and unpushable by clients. | |
| 223 | - audit-relevant activity (issue/MR lifecycle, imports, pushes) lands in | |
| 224 | the =events= table, which also feeds webhooks. | |
| 225 | - the daemon idles under 10MB RSS; the smallest VPS tier is adequate. | |
docs/api.org deleted −127
| @@ -1,127 +0,0 @@ | ||
| 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 | * Commit statuses (CI reporting) | |
| 62 | ||
| 63 | CI reports results through the same command surface (over SSH or the | |
| 64 | JSON API with a full-scope token; reporting requires write access): | |
| 65 | ||
| 66 | #+begin_src sh | |
| 67 | gitbay status set <owner/name> <sha> --context build --state pending | |
| 68 | gitbay status set <owner/name> <sha> --context build --state success --url https://ci.example/run/1 | |
| 69 | gitbay status list <owner/name> <sha> --json # {"combined": "...", "statuses": [...]} | |
| 70 | #+end_src | |
| 71 | ||
| 72 | One row per (commit, context): re-reporting updates in place. States: | |
| 73 | =pending=, =success=, =failure=, =error=; the combined state is the | |
| 74 | worst of them. Statuses appear on commit pages, MR pages, and | |
| 75 | =mr show=. With =repo settings require-checks <repo> on=, merging | |
| 76 | requires the MR head to carry statuses and all of them green. Each | |
| 77 | report also emits a =status= event to webhooks. | |
| 78 | ||
| 79 | * Webhooks | |
| 80 | ||
| 81 | Per-repository outbound POSTs for repository events. Managed by repo | |
| 82 | admins: | |
| 83 | ||
| 84 | #+begin_src sh | |
| 85 | gitbay webhook add <url> --secret s3cret [--events push,issue.created] # default * | |
| 86 | gitbay webhook list | |
| 87 | gitbay webhook deliveries [--limit 50] # status, attempts, last error | |
| 88 | gitbay webhook redeliver <delivery-id> # requeue, including dead letters | |
| 89 | gitbay webhook remove <id> | |
| 90 | #+end_src | |
| 91 | ||
| 92 | ** Events | |
| 93 | ||
| 94 | =push= (data: ref, old, new, forced, deleted), =issue.created=, | |
| 95 | =issue.commented=, =issue.closed=, =issue.open=, =mr.created=, | |
| 96 | =mr.merged= (data: number, sha), =repo.imported= (data: from). | |
| 97 | ||
| 98 | ** Delivery | |
| 99 | ||
| 100 | Each event POSTs one JSON body: | |
| 101 | ||
| 102 | #+begin_src json | |
| 103 | {"event": "push", | |
| 104 | "repo": "you/project", | |
| 105 | "actor": "alice", | |
| 106 | "created_at": "2026-08-24T01:00:00.000Z", | |
| 107 | "data": {"ref": "refs/heads/main", "old": "...", "new": "...", | |
| 108 | "forced": false, "deleted": false}} | |
| 109 | #+end_src | |
| 110 | ||
| 111 | Headers: =X-Gitbay-Event=, =X-Gitbay-Delivery= (id), and — when the hook | |
| 112 | has a secret — =X-Gitbay-Signature-256: sha256=<hex hmac>= over the raw | |
| 113 | body. Verify before trusting: | |
| 114 | ||
| 115 | #+begin_src python | |
| 116 | import hmac, hashlib | |
| 117 | expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest() | |
| 118 | ok = hmac.compare_digest(expected, request.headers["X-Gitbay-Signature-256"]) | |
| 119 | #+end_src | |
| 120 | ||
| 121 | A 2xx within 10 seconds is success. Anything else retries with | |
| 122 | exponential backoff (30s base, doubling) and dead-letters after five | |
| 123 | attempts; =webhook deliveries= shows the trail and =redeliver= revives a | |
| 124 | dead letter. Redirects are never followed, and targets resolving to | |
| 125 | loopback/private/link-local addresses are refused both at registration | |
| 126 | and again at connect time, unless the instance sets | |
| 127 | =[webhooks] allow_local=. | |
docs/roadmap.org deleted −149
| @@ -1,149 +0,0 @@ | ||
| 1 | #+title: gitbay roadmap | |
| 2 | ||
| 3 | Status and direction as of 2026-08-24. Issue numbers reference this | |
| 4 | repository's tracker; this file is the narrative, the tracker is the | |
| 5 | truth. | |
| 6 | ||
| 7 | * Where things stand | |
| 8 | ||
| 9 | Everything in the original plan is built, tested end-to-end against real | |
| 10 | git/ssh/sshd/gpg, and running in production at gitbay.org: the SSH | |
| 11 | control plane (usable from bare OpenSSH, enforced by test), git over | |
| 12 | SSH/HTTPS/git://, signature verification with six states and epoch | |
| 13 | caching, protected branches and =require_signed_commits= (push-time and | |
| 14 | merge-time), issues, merge requests with four merge strategies, orgs | |
| 15 | with membership-derived access, rename/transfer, repo import, invite and | |
| 16 | open registration with SMTP verification, ACME TLS, the read-only and | |
| 17 | accounts web modes, the JSON API fronting the whole command registry, | |
| 18 | signed webhooks with retries and dead-lettering, restore-tested backups, | |
| 19 | the =gitbay= CLI, and docs. The instance hosts 65 repositories including | |
| 20 | this one, and its own development already runs through its issues and | |
| 21 | merge requests. | |
| 22 | ||
| 23 | What it is today: an excellent forge for its author and for CLI-native | |
| 24 | individuals. What it is not yet: a forge a GitHub-habituated *team* | |
| 25 | would stay on, or a project outsiders can easily run themselves. | |
| 26 | ||
| 27 | * Phase 1 — collaboration credibility [COMPLETE 2026-08-24] | |
| 28 | ||
| 29 | Goal: a second contributor works here for a week and misses nothing they | |
| 30 | would act on. All five shipped: deploy keys, commit statuses with | |
| 31 | require-checks gating, email notifications, inline review threads, and | |
| 32 | required approvals with CODEOWNERS and require-resolved. | |
| 33 | ||
| 34 | - [[https://gitbay.org/krz/gitbay/issues/22][#22]] deploy keys — smallest item, unblocks CI checkout; the scope | |
| 35 | already exists in the policy layer | |
| 36 | - [[https://gitbay.org/krz/gitbay/issues/1][#1]] commit statuses API and MR check display | |
| 37 | - [[https://gitbay.org/krz/gitbay/issues/3][#3]] email notifications for issue/MR activity | |
| 38 | - [[https://gitbay.org/krz/gitbay/issues/2][#2]] inline review comments on MR diffs | |
| 39 | - [[https://gitbay.org/krz/gitbay/issues/19][#19]] required approvals and CODEOWNERS (builds on #1 and #2) | |
| 40 | ||
| 41 | * Phase 2 — a product, not a debug view [COMPLETE 2026-08-24; #30 activity graph follows on] | |
| 42 | ||
| 43 | Goal: the site looks and reads like something you would recommend. | |
| 44 | Mostly web-layer; descriptions and profiles already landed as the first | |
| 45 | step. | |
| 46 | ||
| 47 | - [[https://gitbay.org/krz/gitbay/issues/10][#10]] design revamp — closed 2026-08-24 after review (further design work is iteration under new issues) | |
| 48 | — shipped 2026-08-24, open pending visual review | |
| 49 | - [[https://gitbay.org/krz/gitbay/issues/6][#6]] cross-references (#N) and @mentions — done 2026-08-24 | |
| 50 | (rendering-side; backlinks and mention notifications later) | |
| 51 | - [[https://gitbay.org/krz/gitbay/issues/23][#23]] archived repos and topics — done 2026-08-24 (topic | |
| 52 | filtering rides along with search, #7) | |
| 53 | - [[https://gitbay.org/krz/gitbay/issues/24][#24]] blame view — done 2026-08-24 | |
| 54 | - [[https://gitbay.org/krz/gitbay/issues/7][#7]] search — done 2026-08-24 (repo search by name/desc/topic, | |
| 55 | per-repo git grep on web+SSH; cross-repo indexer only if ever needed) | |
| 56 | - [[https://gitbay.org/krz/gitbay/issues/20][#20]] milestones and issue templates — done 2026-08-24 | |
| 57 | - [[https://gitbay.org/krz/gitbay/issues/30][#30]] activity graph on owner pages (platform-recorded activity | |
| 58 | signal; events table already covers issues/MRs, extend to commits) | |
| 59 | - [[https://gitbay.org/krz/gitbay/issues/31][#31]] issue actions from commit messages — done 2026-08-24 | |
| 60 | (closes/fixes/resolves #N closes on landing; bare #N leaves a comment) | |
| 61 | ||
| 62 | * Phase 3 — other people's forges [COMPLETE 2026-08-24] | |
| 63 | ||
| 64 | Goal: someone who is not the author runs an instance and moves their | |
| 65 | work to it. | |
| 66 | ||
| 67 | - [[https://gitbay.org/krz/gitbay/issues/26][#26]] release engineering — done 2026-08-24 except artifact | |
| 68 | hosting, which waits on #8 (go-install vanity live, release.sh, | |
| 69 | CHANGELOG, Homebrew formula in krz/homebrew-tap) | |
| 70 | imports, Homebrew/deb — the adoption gate for everything below | |
| 71 | - [[https://gitbay.org/krz/gitbay/issues/27][#27]] repository maintenance — done 2026-08-24 (admin gc/stats, weekly gitbay-gc.timer) | |
| 72 | - [[https://gitbay.org/krz/gitbay/issues/8][#8]] releases — done 2026-08-24 (notes + assets; v0.1.0 binaries hosted) | |
| 73 | - [[https://gitbay.org/krz/gitbay/issues/17][#17]] issue/PR history import from GitHub — done 2026-08-24 | |
| 74 | - [[https://gitbay.org/krz/gitbay/issues/18][#18]] push/pull mirroring — done 2026-08-24 (worker sync, read-only pull mirrors) | |
| 75 | - [[https://gitbay.org/krz/gitbay/issues/29][#29]] account migration — done 2026-08-24 (bundle export/replay + client-side git mirror; no lock-in, | |
| 76 | ever; the export bundle doubles as a user-level backup) | |
| 77 | - [[https://gitbay.org/krz/gitbay/issues/14][#14]] audit logging and multi-user hardening — done 2026-08-24 (quotas and key-expiry warnings ride with #28) | |
| 78 | - [[https://gitbay.org/krz/gitbay/issues/9][#9]] web signup for open/invite instances — done 2026-08-24 | |
| 79 | ||
| 80 | * Phase 4 — reach | |
| 81 | ||
| 82 | Bigger bets, each valuable independently; order by appetite. | |
| 83 | ||
| 84 | - [[https://gitbay.org/krz/gitbay/issues/13][#13]] CI/CD via external runners (after #1; the forge never executes | |
| 85 | repository content) | |
| 86 | - [[https://gitbay.org/krz/gitbay/issues/16][#16]] Git LFS | |
| 87 | - [[https://gitbay.org/krz/gitbay/issues/15][#15]] static page hosting (needs the separate-origin decision) | |
| 88 | - [[https://gitbay.org/krz/gitbay/issues/11][#11]] iOS app (hutch-based) and [[https://gitbay.org/krz/gitbay/issues/12][#12]] Android | |
| 89 | - [[https://gitbay.org/krz/gitbay/issues/21][#21]] teams within orgs — done 2026-08-25 (members-role scoping + per-repo team grants) | |
| 90 | - [[https://gitbay.org/krz/gitbay/issues/25][#25]] wikis — done 2026-08-25 (push-edited .wiki companions; krz/gitbay has one) | |
| 91 | ||
| 92 | When #15 (pages) and #25 (wikis) land, =docs/= moves out of the blob | |
| 93 | view and becomes gitbay.org's own published documentation site — the | |
| 94 | docs dogfooding the features the same way the tracker and MRs already | |
| 95 | do. | |
| 96 | ||
| 97 | * Phase S — security (cross-cutting) | |
| 98 | ||
| 99 | Not a sequential phase: items land alongside whatever phase is active, | |
| 100 | and the whole set gates flipping gitbay.org to open registration. | |
| 101 | ||
| 102 | - [[https://gitbay.org/krz/gitbay/issues/14][#14]] audit logging, rate limiting, quotas, user disable — the | |
| 103 | multi-user half | |
| 104 | - [[https://gitbay.org/krz/gitbay/issues/28][#28]] hardening umbrella — concrete items done 2026-08-24 | |
| 105 | (umbrella stays open for ongoing work). Both layers landed: | |
| 106 | - software: fuzz targets for every attacker-facing parser (found and | |
| 107 | fixed a decodeArmor slice bug), CSP + security headers, govulncheck | |
| 108 | in =deploy/audit.sh= (fixed circl GO-2026-4550), token comparison | |
| 109 | audit (hash-lookup, no Go-level compare), =docs/threat-model.org=, | |
| 110 | optional minisign over release manifests | |
| 111 | - host: systemd sandbox (=SystemCallFilter=@system-service=, | |
| 112 | =PrivateDevices=, =LockPersonality=, =MemoryDenyWriteExecute=, …), | |
| 113 | unattended-upgrades, fail2ban + =MaxStartups=/=MaxAuthTries= on the | |
| 114 | admin sshd, hourly disk/service/cert monitoring; DB file modes 0750, | |
| 115 | litestream noted for continuous replication. Applied to bay1. | |
| 116 | ||
| 117 | Already true and worth preserving (the threat model will write these | |
| 118 | down): the forge never executes repository content; no server signing | |
| 119 | key; repo-authored HTML never renders on the forge origin; tokens and | |
| 120 | sessions stored as hashes only; SSRF guards at registration and dial | |
| 121 | time; private repositories indistinguishable from nonexistent. | |
| 122 | ||
| 123 | * Explicitly not planned | |
| 124 | ||
| 125 | Recorded so their absence reads as a decision, not an oversight: | |
| 126 | ||
| 127 | - container/package registry — scope creep away from "forge"; external | |
| 128 | registries integrate via CI | |
| 129 | - email patch flow — revisit only if sourcehut-style demand appears | |
| 130 | - federation (ForgeFed) and Postgres — no current need at this scale | |
| 131 | ||
| 132 | * Decisions | |
| 133 | ||
| 134 | - **gitbay.org will eventually be open to all.** (Decided 2026-08-24.) | |
| 135 | Sequencing consequence: before flipping =registration = "open"=, the | |
| 136 | instance needs #14 (audit log, rate limiting, quotas), #9 (web | |
| 137 | signup), an SMTP relay configured for verification mail, and enough | |
| 138 | of Phase 1 that new users get a credible product. Interim step: | |
| 139 | invite mode for early collaborators as soon as [mail] is configured. | |
| 140 | - **Versioning**: semver, starting at v0.1.0 on the current state. | |
| 141 | 0.x signals moving surfaces; =protocol_version= increments only on | |
| 142 | breaking envelope/command changes and is otherwise decoupled from | |
| 143 | release numbers. v1.0.0 when Phase 1 and #26 land. Tags are | |
| 144 | annotated and signed. | |
| 145 | - **Second contributors**: invite mode is the on-ramp (their keys and | |
| 146 | verified emails make signed-main enforceable for them too). A | |
| 147 | CONTRIBUTING file states the workflow: fork on gitbay.org, MR with | |
| 148 | signed commits, =go test ./...= green, review required once #19 | |
| 149 | exists. No CLA — 0BSD needs none; sign-off optional. | |
docs/threat-model.org deleted −92
| @@ -1,92 +0,0 @@ | ||
| 1 | #+title: gitbay threat model | |
| 2 | ||
| 3 | What the forge trusts, what it refuses to do, and where the boundaries | |
| 4 | are. This is the reference for security review; it complements the audit | |
| 5 | log and hardening notes in =admin.org=. | |
| 6 | ||
| 7 | * What gitbay never does | |
| 8 | ||
| 9 | - *Execute repository content.* Git object contents are never run. Hooks | |
| 10 | are gitbay's own binary, invoked by git; they compute facts and ask the | |
| 11 | daemon over a unix socket. Repo files are only ever read. | |
| 12 | - *Hold a signing key.* There is no server-side signing key. "Verified" | |
| 13 | means a signature made by a key the *user* registered — the server | |
| 14 | never vouches for a commit it did not receive already signed. Merge | |
| 15 | commits the server creates are honestly =unsigned=. | |
| 16 | - *Serve repository HTML on its own origin as active content.* Raw file | |
| 17 | serving is =text/plain= with =nosniff=. Rendered markdown/org is | |
| 18 | sanitized (bluemonday) and served under a CSP that forbids scripts. | |
| 19 | - *Put secrets in argv, URLs, or logs.* Import and mirror credentials, | |
| 20 | registration invites, and API tokens travel on stdin or in request | |
| 21 | bodies, never as command arguments (visible in =/proc=) or query | |
| 22 | strings. Tokens are stored only as SHA-256 hashes. | |
| 23 | - *Confirm the existence of private repositories.* Every surface answers | |
| 24 | "not found" identically for a private repo and a nonexistent one — web | |
| 25 | pages, git transport, control commands, release asset downloads. | |
| 26 | ||
| 27 | * Trust boundaries | |
| 28 | ||
| 29 | - *SSH public key = identity.* The SSH username is ignored; the presented | |
| 30 | key's fingerprint resolves to an account. Key uniqueness is global. | |
| 31 | - *Per-instance trust.* Email verification and key registration are local | |
| 32 | to an instance and never transfer. Account migration re-registers keys | |
| 33 | and re-verifies emails on the target by design. | |
| 34 | - *The control plane is one authenticated channel* (SSH), fully usable | |
| 35 | from stock OpenSSH. The JSON API fronts the same command registry with | |
| 36 | bearer tokens minted only over SSH; git transport never runs over it. | |
| 37 | - *Anonymous surfaces* — HTTPS clone of public repos, =git://= where | |
| 38 | enabled, the read-only web UI — carry no credentials and expose only | |
| 39 | public data. HTTP push is refused via a pkt-line =ERR=, never a 401. | |
| 40 | ||
| 41 | * Attacker-controlled parsers | |
| 42 | ||
| 43 | Every parser that eats bytes from a pusher, a key registrant, or an | |
| 44 | anonymous client has a fuzz target and must never panic: | |
| 45 | ||
| 46 | - =internal/protocol= — the SSH command tokenizer (fuzzed against argv | |
| 47 | round-tripping). | |
| 48 | - =internal/gitd= — the =git://= pkt-line reader. | |
| 49 | - =internal/sig= — the commit parser, the SSHSIG armor decoder and blob | |
| 50 | parser, and the OpenPGP armored-key reader. | |
| 51 | ||
| 52 | Run =deploy/audit.sh= to exercise them plus =go vet= and =govulncheck=. | |
| 53 | ||
| 54 | * Secret handling | |
| 55 | ||
| 56 | Tokens (web sessions, login links, email verification, API bearer tokens, | |
| 57 | deploy/CI) are random 256-bit values. Only their SHA-256 hash is stored, | |
| 58 | and verification is a database index lookup on that hash — the secret | |
| 59 | itself is never compared in Go, so there is no timing oracle to exploit. | |
| 60 | Webhook payloads are signed outbound with HMAC-SHA256; the forge verifies | |
| 61 | no inbound HMAC. | |
| 62 | ||
| 63 | * Network-facing request forgery | |
| 64 | ||
| 65 | Anything that makes the *server* open an outbound connection to a | |
| 66 | user-supplied address — webhook delivery, GitHub-history import | |
| 67 | =--api-base=, mirror remotes — passes the same SSRF guard: the scheme | |
| 68 | must be http/https and, unless =webhooks.allow_local= is set, the | |
| 69 | resolved address must not be loopback, private, or link-local. The | |
| 70 | webhook dialer re-checks at connect time so a DNS answer that changes | |
| 71 | after validation still cannot reach private space. Redirects are never | |
| 72 | followed. | |
| 73 | ||
| 74 | * Web responses | |
| 75 | ||
| 76 | Every response carries =Content-Security-Policy= (no scripts, no plugins, | |
| 77 | no embedding; inline styles allowed for chroma and label chips; images | |
| 78 | from any origin so external README images render), =X-Frame-Options: | |
| 79 | DENY=, =X-Content-Type-Options: nosniff=, =Referrer-Policy: no-referrer=, | |
| 80 | and =Strict-Transport-Security= when TLS is on. The UI needs no | |
| 81 | JavaScript, so =script-src 'none'= costs nothing. | |
| 82 | ||
| 83 | * Residual risks, accepted | |
| 84 | ||
| 85 | - External images in rendered READMEs load from their origin (no image | |
| 86 | proxy), which a repo author can use as a tracking pixel against a | |
| 87 | viewer. Documented; proxying is future work. | |
| 88 | - Backups are consistent per the DB-snapshot-first ordering but are not a | |
| 89 | single atomic snapshot; a few orphaned git objects are possible and | |
| 90 | harmless (see =admin.org=). | |
| 91 | - A global signature-verification epoch over-invalidates the cache on any | |
| 92 | trust-input change. Correct, not a leak; a performance tradeoff. | |
docs/users.org deleted −333
| @@ -1,333 +0,0 @@ | ||
| 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 | * Installing the CLI | |
| 9 | ||
| 10 | #+begin_src sh | |
| 11 | go install gitbay.org/gitbay/cmd/gitbay@latest # any platform with Go | |
| 12 | ||
| 13 | brew tap krz/tap https://gitbay.org/krz/homebrew-tap.git | |
| 14 | brew install krz/tap/gitbay # Homebrew (macOS/Linux) | |
| 15 | #+end_src | |
| 16 | ||
| 17 | Or build from source: =go build ./cmd/gitbay= in a clone of | |
| 18 | =https://gitbay.org/krz/gitbay.git=. | |
| 19 | ||
| 20 | * Getting an account | |
| 21 | ||
| 22 | How you join depends on the instance's registration mode. On instances | |
| 23 | with web accounts enabled, =/register= offers the same signup as a | |
| 24 | browser form (paste your SSH public key); everything below works from | |
| 25 | the terminal alone: | |
| 26 | ||
| 27 | - closed :: an admin creates your account on the host and registers your | |
| 28 | first SSH key. Nothing for you to do but hand over your public key. | |
| 29 | - invite :: you receive a single-use code by email. With the SSH key you | |
| 30 | want to use: | |
| 31 | #+begin_src sh | |
| 32 | ssh git@<host> register --username you --invite <code> | |
| 33 | #+end_src | |
| 34 | Your account is active immediately; the invited address is your | |
| 35 | verified email. | |
| 36 | - open :: | |
| 37 | #+begin_src sh | |
| 38 | ssh git@<host> register --username you --email you@example.org | |
| 39 | #+end_src | |
| 40 | A verification code arrives by mail. Until you run | |
| 41 | =ssh git@<host> email verify <code>=, the account is pending: you can | |
| 42 | run =whoami= and the email commands, and nothing else — no git, no | |
| 43 | repos. | |
| 44 | ||
| 45 | * SSH keys | |
| 46 | ||
| 47 | Your key is your identity; there are no passwords anywhere. The SSH | |
| 48 | username is always =git= — the key alone determines who you are. | |
| 49 | ||
| 50 | #+begin_src sh | |
| 51 | gitbay auth keys list | |
| 52 | gitbay auth keys add --scope git < ~/.ssh/ci_key.pub # key on stdin | |
| 53 | gitbay auth keys remove SHA256:... | |
| 54 | #+end_src | |
| 55 | ||
| 56 | Scopes: =full= (default; git plus every control command) or =git= (git | |
| 57 | transport only — right for CI and automation keys, which then cannot | |
| 58 | touch issues, settings, or your account). | |
| 59 | ||
| 60 | A key belongs to exactly one account instance-wide. Registering a key | |
| 61 | someone else already holds is refused without telling you whose it is. | |
| 62 | ||
| 63 | * Verified commits | |
| 64 | ||
| 65 | The commit badge is driven by the *author* email and the signing key: | |
| 66 | =verified= means the signature is valid, the key is registered to an | |
| 67 | account, and the author email is a verified address on that account. | |
| 68 | ||
| 69 | For OpenPGP signing (git's default): | |
| 70 | #+begin_src sh | |
| 71 | gpg --armor --export you@example.org | gitbay auth pgp add | |
| 72 | #+end_src | |
| 73 | ||
| 74 | For SSH signing (=git config gpg.format ssh=): sign with any key | |
| 75 | registered on your account; your verified addresses act as the principal | |
| 76 | set. No separate registration step. | |
| 77 | ||
| 78 | Add and verify additional addresses with =email add <address>= / | |
| 79 | =email verify <code>= (requires the instance to have SMTP; otherwise an | |
| 80 | admin can assert an address for you). | |
| 81 | ||
| 82 | The states you will see, in decreasing order of trust: =verified=, | |
| 83 | =signed_unknown_key= (valid signature, key not registered here — register | |
| 84 | it and history upgrades retroactively), =signed_email_mismatch= (real | |
| 85 | key, author line claims someone else), =signed_key_expired= / | |
| 86 | =signed_key_revoked=, =bad_signature=, =unsigned=. Server-created commits | |
| 87 | (web edits, merge commits) are always =unsigned= — the server holds no | |
| 88 | signing key on principle. | |
| 89 | ||
| 90 | * Repositories | |
| 91 | ||
| 92 | #+begin_src sh | |
| 93 | gitbay repo create you/project [--private] | |
| 94 | gitbay repo clone you/project | |
| 95 | gitbay repo list | |
| 96 | gitbay repo show you/project | |
| 97 | gitbay repo log you/project --limit 20 # commits with signature states | |
| 98 | gitbay repo fork other/project [--name mine] | |
| 99 | gitbay repo delete you/project --yes | |
| 100 | #+end_src | |
| 101 | ||
| 102 | Pushing is SSH-only. Public repositories are anonymously readable over | |
| 103 | HTTPS (and =git://= where enabled); private repositories exist only over | |
| 104 | SSH and answer "not found" to everyone without access. | |
| 105 | ||
| 106 | Access and settings (owner or =admin= grant): | |
| 107 | #+begin_src sh | |
| 108 | gitbay repo access grant you/project alice write # read | write | admin | |
| 109 | gitbay repo access revoke you/project alice | |
| 110 | gitbay repo settings protect you/project main # no force-push, no delete | |
| 111 | gitbay repo settings require-signed you/project on # every commit must verify | |
| 112 | gitbay repo settings git-daemon you/project on # expose over git:// | |
| 113 | gitbay repo topics add you/project cli forge # free-form tags, shown on the web | |
| 114 | gitbay repo search forge # find repos by name/description/topic | |
| 115 | gitbay repo grep you/project "some string" # literal git grep over the default branch | |
| 116 | gitbay repo pin you/project # pin to your web dashboard | |
| 117 | gitbay repo unpin you/project | |
| 118 | gitbay repo archive you/project # read-only: pushes and issue/MR | |
| 119 | gitbay repo unarchive you/project # writes refused, browsing intact | |
| 120 | #+end_src | |
| 121 | ||
| 122 | Import from another forge — git data first, then optionally the GitHub | |
| 123 | issue and PR history (issues keep state/labels/comments; PRs land as | |
| 124 | closed or merged MRs with their discussion; originals are attributed | |
| 125 | inline since foreign authors have no local account; re-running resumes | |
| 126 | where it stopped): | |
| 127 | #+begin_src sh | |
| 128 | gitbay repo import you/mirror --from https://github.com/you/repo.git \ | |
| 129 | [--private] [--token-stdin] # token on stdin, never in the URL | |
| 130 | gitbay repo import-issues you/mirror --from you/repo --token-stdin | |
| 131 | #+end_src | |
| 132 | ||
| 133 | Moving between gitbay instances (no lock-in): run on the TARGET, with | |
| 134 | your key registered on both sides. Profile, repos with settings, | |
| 135 | issues, MRs, and comments replay with attribution; git data mirrors | |
| 136 | client-side through your own key. Keys never transfer and emails | |
| 137 | arrive unverified — trust is per-instance. Re-running resumes. | |
| 138 | Push-blocking policies (require-signed, protected branches) are | |
| 139 | deferred and printed for you to re-apply after the data lands. | |
| 140 | =gitbay auth export= alone doubles as a user-level backup. | |
| 141 | ||
| 142 | #+begin_src sh | |
| 143 | gitbay migrate --from old-instance.example [--from-port 22] | |
| 144 | #+end_src | |
| 145 | ||
| 146 | Mirroring keeps a foreign remote in sync during a gradual migration | |
| 147 | (repo admin; https remotes; the token is stored server-side for the | |
| 148 | recurring sync and never echoed back): | |
| 149 | ||
| 150 | #+begin_src sh | |
| 151 | gitbay repo mirror add you/project https://github.com/you/project.git \ | |
| 152 | --direction push --token-stdin # propagate after every local push | |
| 153 | gitbay repo mirror add you/copy https://github.com/them/theirs.git \ | |
| 154 | --direction pull # follow upstream; local pushes refused | |
| 155 | gitbay repo mirror list # sync status and last error, per mirror | |
| 156 | gitbay repo mirror sync / remove <id> | |
| 157 | #+end_src | |
| 158 | ||
| 159 | * Organizations | |
| 160 | ||
| 161 | Orgs share the owner namespace with users and own repositories at | |
| 162 | =org/repo=. By default members get write on all org repos; org admins | |
| 163 | get repo admin, create repos under the org, and manage membership. | |
| 164 | ||
| 165 | #+begin_src sh | |
| 166 | gitbay org create krz | |
| 167 | gitbay org members add krz alice [--role admin] | |
| 168 | gitbay org show krz | |
| 169 | gitbay org rename krz newname # clone URLs change | |
| 170 | gitbay org delete krz --yes # only when it owns no repositories | |
| 171 | #+end_src | |
| 172 | ||
| 173 | Large orgs scope access with teams: set what plain membership implies, | |
| 174 | then grant per-repo roles through named teams (org admins always keep | |
| 175 | admin; the default =write= keeps the simple model): | |
| 176 | ||
| 177 | #+begin_src sh | |
| 178 | gitbay org settings members-role krz none # write | read | none | |
| 179 | gitbay org team create krz core-devs | |
| 180 | gitbay org team add krz core-devs alice bob # org members only | |
| 181 | gitbay org team grant krz core-devs krz/gitbay write | |
| 182 | gitbay org team show krz core-devs # members + grants | |
| 183 | gitbay org team revoke / remove / delete ... | |
| 184 | #+end_src | |
| 185 | ||
| 186 | * Issues | |
| 187 | ||
| 188 | Anyone who can read a repository can file and comment. Closing/reopening | |
| 189 | is for the author or anyone with write; labels and assignees need write. | |
| 190 | ||
| 191 | #+begin_src sh | |
| 192 | gitbay issue create --title "it breaks" [--body "..." | --file -] | |
| 193 | gitbay issue list [--state open|closed|all] | |
| 194 | gitbay issue show 4 | |
| 195 | gitbay issue comment 4 --message "same here" | |
| 196 | gitbay issue edit 4 --title "better title" [--body|--file -] # author or write | |
| 197 | gitbay issue close 4 / reopen 4 | |
| 198 | gitbay issue label 4 --add bug --remove wontfix | |
| 199 | gitbay issue assign 4 --add alice | |
| 200 | gitbay issue milestone 4 v1.0 # or "none" to clear | |
| 201 | #+end_src | |
| 202 | ||
| 203 | Inside a clone, the repository is inferred from the =origin= remote — | |
| 204 | that is why no =owner/name= appears above. Anywhere else, pass it as the | |
| 205 | first argument. Long text: =--body= inline, =--file -= from stdin, or | |
| 206 | neither on a terminal and =$EDITOR= opens. | |
| 207 | ||
| 208 | Wikis are companion repositories edited by push — no separate storage, | |
| 209 | no web editor, the same rendering pipeline as READMEs (markdown and | |
| 210 | org). Access mirrors the parent repo (read to view, write to push); | |
| 211 | the companion is created on your first push and follows the repo | |
| 212 | through transfer and delete: | |
| 213 | ||
| 214 | #+begin_src sh | |
| 215 | git clone ssh://git@<host>/you/project.wiki.git | |
| 216 | # add Home.md (or .org), more pages, images; relative links between | |
| 217 | # pages just work on the web at /you/project/wiki | |
| 218 | git push | |
| 219 | #+end_src | |
| 220 | ||
| 221 | Releases anchor notes and binary assets to a pushed tag (write access; | |
| 222 | assets stream over SSH, capped by the instance's =max_asset_bytes=): | |
| 223 | ||
| 224 | #+begin_src sh | |
| 225 | gitbay release create v1.0 --title "First light" [--notes|--file -|$EDITOR] | |
| 226 | gitbay release asset add v1.0 tool-linux-amd64 < dist/tool-linux-amd64 | |
| 227 | gitbay release asset get v1.0 tool-linux-amd64 > tool # or the web download link | |
| 228 | gitbay release list / show v1.0 / delete v1.0 --yes | |
| 229 | #+end_src | |
| 230 | ||
| 231 | The web shows them under the repository's =releases= tab with rendered | |
| 232 | notes, sha256 sums, and download links. | |
| 233 | ||
| 234 | Commit messages act on issues when the commits land on the default | |
| 235 | branch (direct push or MR merge): =closes/fixes/resolves #4= closes the | |
| 236 | issue with a linking comment, and a bare =#4= leaves a reference | |
| 237 | comment. Each issue/commit pair acts once, ever. Same repository only. | |
| 238 | ||
| 239 | Milestones group issues and MRs toward a release (write access to | |
| 240 | manage, attach with =issue milestone= / =mr milestone=; progress shows | |
| 241 | on the web at =/owner/name/milestones=): | |
| 242 | ||
| 243 | #+begin_src sh | |
| 244 | gitbay milestone create v1.0 --description "first release" --due 2027-01-01 | |
| 245 | gitbay milestone list [--state open|closed|all] | |
| 246 | gitbay milestone close v1.0 / reopen v1.0 | |
| 247 | #+end_src | |
| 248 | ||
| 249 | Issue templates: commit =.gitbay/issue-template.md= (and optional | |
| 250 | =issue-template-<name>.md= variants) to the default branch. =gitbay | |
| 251 | issue create= prefills =$EDITOR= with the default template, the web | |
| 252 | form prefills its textarea, and =gitbay issue templates= lists them. | |
| 253 | ||
| 254 | * Merge requests | |
| 255 | ||
| 256 | #+begin_src sh | |
| 257 | gitbay mr create --source feature --target main --title "add thing" | |
| 258 | gitbay mr create other/upstream --source you/fork:feature --target main --title "..." | |
| 259 | gitbay mr list / show 4 / diff 4 | |
| 260 | gitbay mr checkout 4 # local branch mr/4 from the MR head | |
| 261 | gitbay mr review 4 --approve # or --request-changes / --comment | |
| 262 | gitbay mr merge 4 [--strategy ff|merge|squash|rebase] | |
| 263 | gitbay mr close 4 | |
| 264 | #+end_src | |
| 265 | ||
| 266 | Semantics worth knowing: | |
| 267 | ||
| 268 | - the MR head lives in the *target* repository as | |
| 269 | =refs/merge-requests/N/head= (fetchable by any reader), so an MR | |
| 270 | survives deletion of its source branch or fork. | |
| 271 | - force-pushing the source updates the MR and marks existing reviews | |
| 272 | stale. | |
| 273 | - default strategy: fast-forward when possible, else a merge commit. | |
| 274 | Squash makes one commit authored by the MR author, committed by the | |
| 275 | merger. Rebase replays a linear range preserving authors; it refuses | |
| 276 | ranges containing merge commits, and when fast-forward is possible it | |
| 277 | *is* one (original commits and signatures land untouched). | |
| 278 | - on =require_signed_commits= branches only fast-forwards of fully | |
| 279 | verified commits merge; everything server-created is refused with | |
| 280 | instructions to rebase locally. | |
| 281 | ||
| 282 | Repo admins can gate merges (=repo settings ...=): =require-approvals | |
| 283 | <n>= (fresh, non-author approvals; each reviewer's latest review is | |
| 284 | their stance, and a fresh request-changes blocks), =require-resolved= | |
| 285 | (no open review threads), =require-checks= (all statuses green). With | |
| 286 | approvals required, a =CODEOWNERS= file on the target branch (root or | |
| 287 | =.gitbay/=) additionally demands an approval from an owner of every | |
| 288 | owned changed file — gitignore-style patterns, last match wins. | |
| 289 | ||
| 290 | Review threads anchor to diff lines: | |
| 291 | ||
| 292 | #+begin_src sh | |
| 293 | gitbay mr diff-comment 4 --path main.go --line 12 --message "use log here" | |
| 294 | gitbay mr diff-comment 4 --reply 7 --message "done" # join thread 7 | |
| 295 | gitbay mr threads 4 # threads with staleness | |
| 296 | gitbay mr resolve 4 7 / unresolve 4 7 | |
| 297 | #+end_src | |
| 298 | ||
| 299 | Threads render inline on the MR page. A force-push marks them stale | |
| 300 | (shown under "threads on earlier revisions") rather than guessing new | |
| 301 | anchors; =mr show= reports the unresolved count. Resolving is for the | |
| 302 | thread author, the MR author, or anyone with write. | |
| 303 | ||
| 304 | * Notifications | |
| 305 | ||
| 306 | When the instance has SMTP configured, activity mails you: someone | |
| 307 | opens an issue or MR on your repository, comments where you are a | |
| 308 | participant (author, commenter, reviewer), reviews, closes, or merges. | |
| 309 | You are never mailed about your own actions, and only verified primary | |
| 310 | addresses receive anything. Delivery retries on relay failure. | |
| 311 | ||
| 312 | * Scripting | |
| 313 | ||
| 314 | Every read command takes =--json= and emits one envelope: | |
| 315 | ={"protocol_version": 1, "data": ...}=. stdout is data, stderr is | |
| 316 | messages. Exit codes are stable: 0 ok, 1 failure, 2 usage, 3 not found, | |
| 317 | 4 denied, 5 server/protocol error. Nothing ever prompts; destructive | |
| 318 | commands take =--yes=. | |
| 319 | ||
| 320 | For HTTP automation see =docs/api.org=. | |
| 321 | ||
| 322 | * CLI setup | |
| 323 | ||
| 324 | #+begin_src sh | |
| 325 | gitbay remote add myforge forge.example.org [--port n] [--user u] --default | |
| 326 | gitbay remote list | |
| 327 | gitbay init [name] [--private] # git init + repo create + origin, in one step | |
| 328 | #+end_src | |
| 329 | ||
| 330 | Configuration lives at =~/.config/gitbay/config.toml=. The CLI shells | |
| 331 | out to your real =ssh=, so =~/.ssh/config=, the agent, and hardware keys | |
| 332 | all apply. Man pages: =gitbay man --dir <dir>=; completions: | |
| 333 | =gitbay completion bash|zsh|fish=. | |