.gitbay/wiki/Users.org

e2a32d5f8d59e4213571c602bd9009b6c8fa86ed
gitbay/.gitbay/wiki/Users.org rendered · source · history · blame · raw

828 lines · 38910 bytes

  1#+title: gitbay user guide
  2
  3Everything here works from stock OpenSSH — replace =gitbay= with
  4=ssh git@<host>= in any command. The CLI adds convenience (instance
  5profiles, repo inference, =$EDITOR=), nothing more. Two things differ
  6over bare ssh:
  7
  8- ssh joins its arguments with spaces and the server tokenizes the
  9  result, so a value with a space in it needs a second layer of quotes:
 10  =--description "'a widget'"=, not =--description "a widget"=, which
 11  arrives as two arguments.
 12- =--help= is not understood. =ssh git@<host> help= lists every command
 13  the server knows, and =help <noun>= (=help repo=, =help mr=) prints
 14  the usage of each command under that noun.
 15
 16* Installing the CLI
 17
 18#+begin_src sh
 19go install gitbay.org/gitbay/cmd/gitbay@latest   # any platform with Go
 20
 21brew tap krz/tap https://gitbay.org/krz/homebrew-tap.git
 22brew install krz/tap/gitbay                      # Homebrew (macOS/Linux)
 23#+end_src
 24
 25Or build from source: =go build ./cmd/gitbay= in a clone of
 26=https://gitbay.org/krz/gitbay.git=.
 27
 28* Getting an account
 29
 30How you join depends on the instance's registration mode. On instances
 31with web accounts enabled, =/register= offers the same signup as a
 32browser form (paste your SSH public key); everything below works from
 33the terminal alone:
 34
 35- closed :: an admin creates your account on the host and registers your
 36  first SSH key. Nothing for you to do but hand over your public key.
 37- invite :: you receive a single-use code by email. With the SSH key you
 38  want to use:
 39  #+begin_src sh
 40  ssh git@<host> register --username you --invite <code>
 41  #+end_src
 42  Your account is active immediately; the invited address is your
 43  verified email.
 44- open ::
 45  #+begin_src sh
 46  ssh git@<host> register --username you --email you@example.org
 47  #+end_src
 48  A verification code arrives by mail. Until you run
 49  =ssh git@<host> email verify <code>=, the account is pending: you can
 50  run =whoami= and the email commands, and nothing else — no git, no
 51  repos.
 52
 53* SSH keys
 54
 55New to SSH keys? [[SSH-keys]] makes one in two commands.
 56
 57Your key is your identity; there are no passwords anywhere. The SSH
 58username is always =git= — the key alone determines who you are.
 59
 60#+begin_src sh
 61gitbay auth keys list
 62gitbay auth keys add --scope git < ~/.ssh/ci_key.pub   # key on stdin
 63gitbay auth keys add --label laptop < ~/.ssh/id_ed25519.pub
 64gitbay auth keys label SHA256:... "work laptop"
 65gitbay auth keys remove SHA256:...
 66#+end_src
 67
 68A key's label is the comment on its =authorized_keys= line unless
 69=--label= gives one; =keys label= renames a key, and with no text
 70clears the name. Labels are one line of up to 64 bytes.
 71
 72Removing a key closes every connection it opened, including the CLI's
 73shared one; removing the key the current command runs on ends that
 74command's connection too.
 75
 76Scopes: =full= (default; git plus every control command), =git= (git
 77transport only — right for automation keys, which then cannot touch
 78issues, settings, or your account), or =runner= (the CI runner's
 79protocol plus read-only git, for the key a =gitbay-runner= host holds;
 80see [[Admin]]).
 81
 82A key belongs to exactly one account instance-wide. Registering a key
 83someone else already holds is refused without telling you whose it is.
 84
 85* Verified commits
 86
 87The commit badge is driven by the *author* email and the signing key:
 88=verified= means the signature is valid, the key is registered to an
 89account, and the author email is a verified address on that account.
 90
 91For OpenPGP signing (git's default):
 92#+begin_src sh
 93gpg --armor --export you@example.org | gitbay auth pgp add
 94#+end_src
 95
 96For SSH signing (=git config gpg.format ssh=): sign with any key
 97registered on your account; your verified addresses act as the principal
 98set. No separate registration step.
 99
100Add and verify additional addresses with =email add <address>= /
101=email verify <code>= (requires the instance to have SMTP; otherwise an
102admin can assert an address for you). =email list= shows them. =email
103remove <address>= drops one, with two refusals: the primary stays until
104=email primary <address>= names another verified address, and the last
105verified address stays, since activation, login links and commit
106identity all resolve through verified addresses. Removing a verified
107address re-evaluates signature states, so a commit authored from it
108shows as =signed_email_mismatch= afterwards. A removed address is free
109for any account to claim.
110
111The states you will see, in decreasing order of trust: =verified=,
112=signed_unknown_key= (valid signature, key not registered here — register
113it and history upgrades retroactively), =signed_email_mismatch= (real
114key, author line claims someone else), =signed_key_expired= /
115=signed_key_revoked=, =bad_signature=, =unsigned=. Server-created commits
116(web edits, merge commits) are always =unsigned= — the server holds no
117signing key on principle.
118
119=repo bookmark <owner/name>= saves a repository to come back to and
120=repo unbookmark= drops it; =repo bookmarks= lists yours, each with how
121many people have bookmarked it. The web has the control on the
122repository header, the count in the facts bar, and the list at
123=/bookmarks=. Bookmarks are public and counted; pins are private and
124drive the rail. Read access is all a bookmark needs.
125
126A job's result is a property of the commit's *tree*, not its sha: a
127rebase onto a base that touched nothing the branch did gives every
128commit a new sha and the same tree, and a job that already passed for
129that tree is not run again — the new commit gets the earlier result as
130its =ci/<job>= status, naming the build it came from. Scheduled and tag
131jobs are never reused this way; their trigger is the clock or the tag.
132
133* Profiles
134
135A profile is what =/{owner}= shows: a one-line description, a website, a
136set of links, long-form about text, the repositories you can see, org
137membership, and a year of activity. Users and orgs have the same fields.
138Repository rows carry the listing metadata too — topics, license,
139default branch, last commit — so a client renders a profile listing the
140way =/{owner}= does. The web dispatches this command rather than
141assembling the page itself.
142
143#+begin_src sh
144gitbay profile show               # your own
145gitbay profile show alice
146gitbay profile set --description "builds small tools" --website https://alice.example
147#+end_src
148
149The about text is not a field. It is =profile/README.md= or
150=profile/README.org= on the default branch of =<owner>/.gitbay=, a
151repository you own like any other, written by a push or by
152=repo commit-file=:
153
154#+begin_src sh
155gitbay repo create alice/.gitbay
156gitbay repo commit-file alice/.gitbay profile/README.org \
157       --ref main --file - < about.org
158#+end_src
159
160The extension picks the renderer; =.md=, =.org= and =.markdown= are
161resolved in that order, so a =README.md= beside a =README.org= wins.
162=profile show= reports the text as =about=, its format as
163=about_format=, and the file it came from as =about_path=.
164
165Access follows the repository: a private =.gitbay= keeps the about text
166to you and the admins. A repository whose name starts with a dot is
167infrastructure rather than a project, so it stays out of =explore= and
168off the profile's repository list — =repo list= still shows it, and it
169is reachable at its own URL.
170
171Up to five links, each =label|url= or a bare url, http(s) only. Passing
172=--link= replaces the whole set; a single empty one clears it:
173
174#+begin_src sh
175gitbay profile set --link "Mastodon|https://fosstodon.example/@alice" \
176                   --link https://alice.example/now
177gitbay profile set --link ""      # clear
178#+end_src
179
180Every field follows the same rule as the rest of the CLI: a flag you
181leave out is untouched, and ='' clears the one you name. Org profiles
182work the same way and need org admin:
183
184#+begin_src sh
185gitbay org profile krz --description "software and experiments"
186gitbay org profile krz            # no flags shows it
187#+end_src
188
189An org's about text works the same way, in =<org>/.gitbay=.
190
191The settings page edits description, website and links, and points at the
192about file — with a button that creates =<owner>/.gitbay= and its first
193README when you have none, so the file editor has a branch to open. The
194JSON API runs =profile set= like any other write command.
195
196* Repositories
197
198#+begin_src sh
199gitbay repo create you/project [--private]
200gitbay repo clone you/project
201gitbay repo list
202gitbay repo show you/project
203gitbay repo log you/project --limit 20      # commits with signature states
204gitbay repo fork other/project [--name mine]
205gitbay repo rename you/project tool          # clone URLs change
206gitbay repo delete you/project --yes
207#+end_src
208
209=repo show= also reports your own state on the repository — =watch=
210(=watching= or =muted=; =repo unwatch= clears either) and =bookmarked= —
211and =fork_of= when it is a
212fork whose parent you can read, so a client draws a toggle rather than
213two blind buttons. Absent means none.
214
215Pushing is SSH-only. Public repositories are anonymously readable over
216HTTPS (and =git://= where enabled); private repositories exist only over
217SSH and answer "not found" to everyone without access.
218
219Access and settings (owner or =admin= grant):
220#+begin_src sh
221gitbay repo access grant you/project alice write    # read | write | admin
222gitbay repo access revoke you/project alice
223gitbay repo access list you/project                 # everyone who can reach it, role, and via what
224gitbay repo settings protect you/project main       # no force-push, no delete
225gitbay repo settings require-mr you/project on      # protected branches: merge requests only
226gitbay repo settings protect-tag you/project 'v*'   # matching tags: created once, never moved or deleted
227gitbay repo settings default-branch you/project trunk # HEAD, and what the web shows
228gitbay repo settings require-signed you/project on  # every commit must verify
229gitbay repo settings git-daemon you/project on      # expose over git://
230gitbay repo topics add you/project cli forge        # free-form tags, shown on the web
231gitbay repo search forge                            # find repos by name/description/topic
232gitbay repo grep you/project "some string"          # literal git grep over the default branch
233gitbay repo pin you/project                         # pin to your web dashboard
234gitbay repo unpin you/project
235gitbay repo archive you/project                     # read-only: pushes and issue/MR
236gitbay repo unarchive you/project                   #   writes refused, browsing intact
237#+end_src
238
239Import from another forge — git data first, then optionally the issue
240and PR history from GitHub or any Forgejo instance such as Codeberg
241(issues keep state/labels/comments; PRs land as closed or merged MRs
242with their discussion; originals are attributed inline since foreign
243authors have no local account; re-running resumes where it stopped):
244#+begin_src sh
245gitbay repo import you/mirror --from https://github.com/you/repo.git \
246    [--private] [--token-stdin]        # token on stdin, never in the URL
247gitbay repo import-issues you/mirror --from you/repo --token-stdin
248gitbay repo import-issues you/mirror --from you/repo \
249    --api-base https://codeberg.org/api/v1    # Forgejo: the site's /api/v1
250#+end_src
251
252Sourcehut has no API of that shape; =repo import= takes its git data
253and the todo.sr.ht tracker is not read.
254
255Moving between gitbay instances (no lock-in): run on the TARGET, with
256your key registered on both sides. Profile, repos with settings,
257issues, MRs, and comments replay with attribution; git data mirrors
258client-side through your own key. Keys never transfer and emails
259arrive unverified — trust is per-instance. Re-running resumes.
260Push-blocking policies (require-signed, protected branches) are
261deferred and printed for you to re-apply after the data lands.
262=gitbay auth export= alone doubles as a user-level backup.
263
264#+begin_src sh
265gitbay migrate --from old-instance.example [--from-port 22]
266#+end_src
267
268Mirroring keeps a foreign remote in sync during a gradual migration
269(repo admin; https remotes; the token is stored server-side for the
270recurring sync and never echoed back):
271
272#+begin_src sh
273gitbay repo mirror add you/project https://github.com/you/project.git \
274    --direction push --token-stdin    # propagate after every local push
275gitbay repo mirror add you/copy https://github.com/them/theirs.git \
276    --direction pull                  # follow upstream; local pushes refused
277gitbay repo mirror list               # sync status and last error, per mirror
278gitbay repo mirror sync / remove <id>
279#+end_src
280
281Markdown and org files render on the web when opened, the way a README
282does on the repository page, with relative links resolved against the
283file's directory; =source= in the file's action bar (or =?view=source=)
284shows the text instead. Other files show the text with highlighting.
285
286* Organizations
287
288Orgs share the owner namespace with users and own repositories at
289=org/repo=. By default members get write on all org repos; org admins
290get repo admin, create repos under the org, and manage membership. An
291org's existence and member list are public; its teams are visible to
292members, and anyone else asking is refused rather than told the org
293does not exist.
294
295#+begin_src sh
296gitbay org create krz
297gitbay org members add krz alice [--role admin]
298gitbay org show krz
299gitbay org rename krz newname     # clone URLs change
300gitbay org delete krz --yes       # only when it owns no repositories
301#+end_src
302
303Large orgs scope access with teams: set what plain membership implies,
304then grant per-repo roles through named teams (org admins always keep
305admin; the default =write= keeps the simple model):
306
307#+begin_src sh
308gitbay org settings members-role krz none   # write | read | none
309gitbay org team create krz core-devs
310gitbay org team add krz core-devs alice bob # org members only
311gitbay org team grant krz core-devs krz/gitbay write
312gitbay org team show krz core-devs          # members + grants
313gitbay org team revoke / remove / delete ...
314#+end_src
315
316* Issues
317
318Anyone who can read a repository can file and comment. Closing/reopening
319is for the author or anyone with write; labels and assignees need write.
320
321#+begin_src sh
322gitbay issue create --title "it breaks" [--body "..." | --file -]
323gitbay issue list [--state open|closed|all]
324gitbay issue show 4
325gitbay issue comment 4 --message "same here"
326gitbay issue edit 4 --title "better title" [--body|--file -]  # author or write
327gitbay issue close 4 / reopen 4
328gitbay issue label 4 --add bug --remove wontfix
329gitbay issue assign 4 --add alice
330gitbay issue milestone 4 v1.0                 # or "none" to clear
331#+end_src
332
333Inside a clone, the repository is inferred from the =origin= remote —
334that is why no =owner/name= appears above. Anywhere else, pass it as the
335first argument. Long text: =--body= inline, =--file -= from stdin, or
336neither on a terminal and =$EDITOR= opens.
337
338A wiki is the =.gitbay/wiki/= directory on the default branch — no
339separate repository, the same rendering pipeline as READMEs (markdown
340and org). Editing it is editing a file: a push, =repo commit-file=, or
341the web editor on a repository that permits server-authored commits (one
342requiring signed commits is push-only, since the server signs nothing):
343
344#+begin_src sh
345mkdir -p .gitbay/wiki
346# add Home.md (or .org), more pages, images; relative links between
347# pages just work on the web at /you/project/wiki
348git add .gitbay/wiki && git commit -m "wiki" && git push
349#+end_src
350
351Pages may sit in subfolders. A nested page is named by its path —
352=.gitbay/wiki/Guide/Install.md= is =/you/project/wiki/Guide/Install= and
353=wiki show you/project Guide/Install= — and the sidebar groups it under
354its folder. A relative link or image on a nested page resolves from the
355page's own folder first, then from the top of the wiki, so a link to
356=Home= still reaches the top-level page. SVG images render.
357
358Releases anchor notes and binary assets to a pushed tag (write access;
359assets stream over SSH, capped by the instance's =max_asset_bytes=).
360While a release exists its tag can be neither deleted nor moved, whether
361or not the tag is protected: delete the release first.
362
363#+begin_src sh
364gitbay release create v1.0 --title "First light" [--notes|--file -|$EDITOR]
365gitbay release asset add v1.0 tool-linux-amd64 < dist/tool-linux-amd64
366gitbay release asset get v1.0 tool-linux-amd64 > tool   # or the web download link
367gitbay release list / show v1.0 / delete v1.0 --yes
368#+end_src
369
370The web shows them under the repository's =releases= tab with rendered
371notes, sha256 sums, and download links. Feed readers subscribe at
372=/you/project/releases.atom=; commits are at =/you/project/log.atom=
373(the default branch) or =/you/project/log.atom/<ref>=, and an owner's
374activity on their public repositories at =/you/activity.atom=. The
375pages carry the discovery link.
376
377Commit messages act on issues when the commits land on the default
378branch (direct push, MR merge, =repo commit-file= or the web editor):
379=closes/fixes/resolves #4= closes the
380issue with a linking comment, and a bare =#4= leaves a reference
381comment. Each issue/commit pair acts once, ever. =Closes owner/name#N= closes an issue in another repository when
382you hold write there with a key whose scope reaches it; otherwise it
383stays a plain link. A deploy key closes nothing outside the repository
384it is bound to, but naming that repository by full path works as the
385bare form does. The comment left
386on the closed issue links the closing commit by its repository path, so
387closing a public repository's issue from a private one names the private
388repository there. A bare =owner/name#N= links and does nothing.
389
390Milestones group issues and MRs toward a release (write access to
391manage, attach with =issue milestone= / =mr milestone=; progress shows
392on the web at =/owner/name/milestones=):
393
394#+begin_src sh
395gitbay milestone create v1.0 --description "first release" --due 2027-01-01
396gitbay milestone list [--state open|closed|all]
397gitbay milestone close v1.0 / reopen v1.0
398#+end_src
399
400An org holds labels and milestones every repository under it sees
401beside its own. =issue label --add=, =mr label --add=, =issue
402milestone= and =mr milestone= resolve the org's row first; a repository
403cannot create a label or milestone with a name its org holds. Creating
404an org label or milestone whose name repositories under the org already
405use folds them in: their issues and merge requests move to the org's
406row. Org admins manage them; counts span the repositories you can read.
407
408#+begin_src sh
409gitbay org label set acme bug --color cf222e
410gitbay org label list acme / remove acme bug
411gitbay org milestone create acme v2 --due 2027-03-01
412gitbay org milestone list acme [--state open|closed|all]
413gitbay org milestone close acme v2 / reopen acme v2
414#+end_src
415
416On the web: =/acme/-/labels= and =/acme/-/milestones=, read-only.
417
418Issue templates: commit =.gitbay/issue-template.md= (and optional
419=issue-template-<name>.md= variants) to the default branch. =gitbay
420issue create= prefills =$EDITOR= with the default template, the web
421form prefills its textarea, and =gitbay issue templates= lists them.
422
423Lists narrow the same way on every surface: =issue list --label bug
424--assignee bob --author alice --milestone v1= (or =--milestone none=),
425=mr list --label bug --author bob --milestone v1=; the web's issue and
426merge request lists take the same names as query parameters, and each
427active filter shows with a link that drops it.
428
429Labels take a colour: =gitbay label set bug --color cf222e=; =label
430list= shows each with its colour and how many issues and merge requests
431carry it, and =label remove= takes one off all of them. =issue label
432--add= and =mr label --add= still create a colourless label on the fly.
433
434* Merge requests
435
436#+begin_src sh
437gitbay mr create --source feature --target main --title "add thing"
438gitbay mr create other/upstream --source you/fork:feature --target main --title "..."
439gitbay mr list / show 4 / diff 4
440gitbay mr checkout 4              # local branch mr/4 from the MR head
441gitbay mr review 4 --approve      # or --request-changes / --comment
442gitbay mr label 4 --add bug --remove wontfix
443gitbay mr merge 4 [--strategy ff|merge|squash|rebase]
444gitbay mr close 4
445#+end_src
446
447A merge request closed without merging can name the one that carries
448its change forward: =mr close 4 --by 7= records it and both pages show
449it, and =mr edit 4 --superseded-by 7|none= sets or clears it
450afterwards, refused on anything but a closed merge request.
451
452The web list shows each request's combined check state and comment
453count alongside its title, so open work needing attention stands out
454without opening it.
455
456Semantics worth knowing:
457
458- the MR head lives in the *target* repository as
459  =refs/merge-requests/N/head= (fetchable by any reader), so an MR
460  survives deletion of its source branch or fork. It is never removed
461  on merge or close; after a history rewrite an instance admin can
462  drop it with =admin mr prune=.
463- force-pushing the source updates the MR and marks existing reviews
464  stale — unless the diff is the one they reviewed. A rebase onto a
465  target that moved on changes every sha and nothing about the change,
466  so fresh reviews follow it to the new head; a push that changes the
467  diff stales them.
468- default strategy: fast-forward when possible, else a merge commit.
469  Squash makes one commit authored by the MR author, committed by the
470  merger. Rebase replays a linear range preserving authors; it refuses
471  ranges containing merge commits, and when fast-forward is possible it
472  *is* one (original commits and signatures land untouched).
473- on =require_signed_commits= branches only fast-forwards of fully
474  verified commits merge; everything server-created is refused with
475  instructions to rebase locally. =gitbay mr rebase <n>= is those
476  instructions: in a clone of the target it replays the source branch
477  onto the target and force-pushes it, then =mr merge <n>= fast-forwards.
478  The replay is local, so the commits carry your signature and not the
479  server's — it holds no key. A branch living in a fork is refused, with
480  the repository to run it in; rebase it there.
481
482Repo admins can gate merges (=repo settings ...=): =require-approvals
483<n>= (fresh, non-author approvals; each reviewer's latest review is
484their stance, and a fresh request-changes blocks), =require-resolved=
485(no open review threads), =require-checks= (all statuses green), and
486=require-codeowners= (an approval from an owner of every owned changed
487file). Owners come from a =CODEOWNERS= file on the target branch, root
488or =.gitbay/=, gitignore-style patterns, last match wins. The toggle is
489the opt-in, so a repository can carry the file as documentation of who
490to ask without it gating merges; with it on and no file on the target
491branch, the merge is refused and says so. It does not wait on
492=require-approvals=.
493
494=mr show= carries a =gates= block with where the merge request stands
495against all of them — approvals counted and required, owners still
496outstanding per file, open threads, checks, and whether a fast-forward
497is possible — and the merge request page shows the same block. =mr
498merge= names every unmet gate at once rather than the first. =mr
499review= says when a verdict is advisory, which it is from anyone
500without write access: the gates do not count it.
501
502Those gates apply to =mr merge=. A direct push to a protected branch
503passes none of them until =require-mr on=: then an existing protected
504branch refuses every push, including =repo commit-file= and the web
505editor, and the server's merge is its only writer. Creating the branch
506is still a push, since there is nothing to open a merge request against
507yet.
508
509A merge request whose target is another open merge request's source
510branch is stacked on it: =mr create= says so, =mr show= carries
511=stacked_on= and =stacked=, and merging the lower one retargets the
512upper onto =main= with its reviews kept. See [[Stacked-MRs][Stacked
513merge requests]].
514
515Review threads anchor to diff lines:
516
517#+begin_src sh
518gitbay mr diff-comment 4 --path main.go --line 12 --message "use log here"
519gitbay mr diff-comment 4 --reply 7 --message "done"     # join thread 7
520gitbay mr threads 4                                     # threads with staleness
521gitbay mr resolve 4 7 / unresolve 4 7
522#+end_src
523
524Threads render inline on the MR page. A force-push marks them stale
525(shown under "threads on earlier revisions") rather than guessing new
526anchors; =mr show= reports the unresolved count. Resolving is for the
527thread author, the MR author, or anyone with write.
528
529* CI builds
530
531A =.gitbay/ci.yml= in the repo runs jobs on every branch push:
532
533#+begin_src yaml
534jobs:
535  test:
536    steps:
537      - go test ./...
538#+end_src
539
540A config holds at most ten jobs of fifty steps each, each step under
5414096 bytes, in a file under 64 KiB; a longer step runs a script from the
542repository (=- sh ci/publish.sh=). A config past a cap, or otherwise
543unparseable, records a failed =ci/config= status on the commit and
544queues nothing, so look there when a push builds nothing.
545
546Each job becomes a build (=build list=, =build log=, the builds tab on
547the web) and a =ci/<job>= commit status, which =repo settings
548require-checks= can gate merges on. =build list= takes =--ref=,
549=--status= and =--job= to narrow the listing, combinable; the builds
550tab reads the same flags from its =?ref=, =?status= and =?job= query
551parameters and groups the result into one row per commit. Steps run
552with =sh -c= on the instance's runner, stopping at the first failure;
553a broken config surfaces as a failed =ci/config= status. Environment:
554=GITBAY_REPO=,
555=GITBAY_SHA=, =GITBAY_REF=, =GITBAY_JOB=, =CI=true=, and =GITBAY_SSH=,
556the instance's ssh destination as the build reaches it (=git@gitbay.org=
557from a runner elsewhere; inside a container on the server's own runner
558the host is at a private address the runner fills in). A job that
559talks back to the instance — a release asset, a comment, a push to a
560pages branch — uses =$GITBAY_SSH= with a key it holds as a secret;
561the build's container has no key of its own. Two things about that
562container: a secret with newlines (a private key) arrives intact, and
563=ssh= expands =~= from the passwd entry, =/root=, not from =$HOME=,
564which is the build home — so keep an ssh config in the workspace and
565pass it with =ssh -F= (and =GIT_SSH_COMMAND="ssh -F ..."= for git),
566which also works on a runner with no container, where writing to
567=~/.ssh= would edit that machine's own configuration.
568
569Secrets: =repo secret set <owner/name> <NAME>= reads the value from
570stdin (never argv) and injects it into the repo's builds as =$NAME=;
571=repo secret list= shows names only, and the value is never echoed
572back. Anyone with write access can read a secret from inside a build,
573so scope them accordingly.
574
575Schedules: a job with =schedule: "17 11,23 * * *"= (five-field cron,
576server-local time; lists, ranges, and steps supported) runs on its cron
577against the default branch instead of on push. A default-branch push
578registers or updates the schedule. A job with =tags: "v*"= runs when a
579matching tag is pushed — and only then; =schedule= and =tags= are
580mutually exclusive. =build trigger <owner/name> <job>= queues any job
581immediately, and =build cancel <owner/name> <n>= withdraws one, queued
582or running: a running build stops at the runner within seconds and the
583log says who cancelled it. Both need write access.
584
585What each push shape queues, with dedupe, path filters, schedules and
586the reaper together, is one table on [[CI][CI]].
587
588** Your own runner
589
590Builds run on runners attached to the repository. An instance need not
591offer any: install =gitbay-runner= on a machine of yours and attach it.
592
593#+begin_src sh
594brew install krz/tap/gitbay-runner        # or a binary from the release
595gitbay-runner init -remote git@gitbay.org
596#+end_src
597
598=init= generates a key under =~/.config/gitbay-runner/=, writes
599=config.toml= beside it, and prints the public key with the command to
600attach it:
601
602#+begin_src sh
603gitbay repo runner add owner/name < ~/.config/gitbay-runner/id_ed25519.pub
604#+end_src
605
606or paste the key under Runners on the repository's settings page. Then
607=brew services start krz/tap/gitbay-runner=, or run =gitbay-runner= with
608no arguments; it reads the config file, and any flag overrides it.
609
610What it builds: every build for the repositories it is attached to,
611with the repository's secrets, and nothing else. Merge requests from
612forks are untrusted and wait unless the runner runs with =-untrusted=,
613which is only sensible with =-isolation podman -image <ref>= (see
614[[Admin][Admin]]). Attach one runner to several repositories by repeating
615=repo runner add=; run several runners on one account by running =init=
616on each machine. =repo runner list= shows each attached key, when it
617last polled and the build it holds; =repo runner remove <fingerprint>=
618detaches one (the key stays on your account; =keys remove= drops it).
619A runner key reaches only the runner protocol and read-only git, so a
620build step that reads it off disk cannot administer your account.
621
622* Large files (LFS)
623
624Standard Git LFS works over both transports with no setup beyond the
625usual =git lfs track=. SSH remotes authenticate through
626=git-lfs-authenticate= (deploy keys included: ro keys can download, rw
627keys upload); anonymous HTTPS clones of public repositories can fetch
628LFS objects with no credentials. Objects are verified against their
629sha256 on upload and capped at 512MB by default.
630
631* Pages
632
633On instances with =[pages] domain= set, a =pages= branch in any public
634repo is served as a static site: the repo named =pages= at
635=https://<you>.<domain>/=, every other repo at
636=https://<you>.<domain>/<repo>/=. Push HTML to publish; a CI job can
637build and push the branch for automatic deploys. Sites run on a
638separate origin — your scripts work, and the forge's cookies are out of
639reach.
640
641A repo can also serve its pages branch on a domain you own.
642=repo domain add <owner/name> <domain>= claims it and prints a DNS TXT
643challenge (=_gitbay-challenge.<domain>=); create the record, run
644=repo domain verify=, then point the domain's A/AAAA records at the
645instance (DNS-only if the domain sits behind a proxying provider — the
646instance issues its own certificates). Claims are exclusive per
647instance; unverified claims serve nothing and expire after 7 days.
648=repo domain list= reports pending/verified/expired.
649
650* Snippets
651
652A snippet is one or more named text files you own outside any
653repository, for a log or a fragment shared by URL. Create one from a
654file on stdin; the reply is the id and the URL:
655
656#+begin_src sh
657gitbay snippet create build.log --description "failing build" < build.log
658gitbay snippet file set <id> notes.txt < notes.txt   # add or replace a file
659gitbay snippet file get <id> build.log > build.log
660gitbay snippet edit <id> --visibility public
661gitbay snippet list                                  # yours
662gitbay snippet list <owner>                          # their public ones
663gitbay snippet delete <id>
664#+end_src
665
666Visibility is =public= (listed on your page), =unlisted= (anyone with
667the URL, listed nowhere; the default) or =private= (you alone; not
668found to everyone else). Files are text, valid UTF-8, each under the
669instance's =max_snippet_bytes=, at most 64 per snippet. A snippet keeps
670at least one file. There is no history: setting a file replaces it.
671
672On the web, =/<you>/-/snippets= lists yours, each snippet page renders
673its files with a raw link per file, and the same page creates, edits
674and deletes through the commands above.
675
676* Browser sessions
677
678=gitbay web login= mints a one-time URL; the session it opens lasts
679seven days. =gitbay web sessions list= shows each of yours by a short
680id with its creation and expiry, and =gitbay web sessions revoke <id>=
681or =--all= ends them from the terminal, which is where a lost laptop is
682handled.
683
684=web theme set light= or =dark= fixes the web UI's colour scheme for
685your account; =system=, the default, follows the browser's own
686preference. =web theme show= prints it. The account page has the same
687control under Appearance.
688
689* Notifications
690
691When the instance has SMTP configured, activity mails you as well as
692filing the inbox row: someone opens an issue or MR on your repository,
693comments where you are a participant (author, commenter, reviewer, or
694mentioned), reviews, closes, merges, or assigns you.
695=notifications settings mail off= keeps the inbox and stops that mail
696(login links are not activity and still arrive); the account page has
697the same switch.
698=notifications settings watch on= makes you a recipient of every issue
699and merge request on the repositories you can write to, as if you had
700run =repo watch= on each: it is consulted when a notice is delivered,
701so a grant or a revoke needs no watch row, and an explicit watch or
702mute on a repository still wins. Off by default; the account page has
703this switch too. Writing =@name= in an issue, merge request or
704comment files "mentioned you" in that account's inbox and makes them a
705participant of the thread, provided they can read the repository and
706have not muted it; watchers are not told about a mention addressed to
707someone else.
708You are never mailed about your own actions, and only verified primary
709addresses receive anything. Delivery retries on relay failure.
710
711Push is the same activity again, delivered to a phone: the iOS app
712registers a device, and =notifications settings push off= silences it
713the way =mail off= silences mail, without deregistering anything.
714=notifications device list= shows what is registered (token shown
715truncated); =notifications device remove <id>= drops one by hand, from
716the CLI or the account page. A device is also dropped on its own the
717moment Apple reports the token dead, so an app deleted from a phone
718stops costing anything without you having to notice. Push only works
719when the instance operator has configured it: on an instance with
720=[push] enabled = false=, =notifications device add= refuses rather
721than registering a device nothing can deliver to.
722
723Push notifications carry the notice in full: a private repository's
724name and the issue or merge request number reach Apple and can appear
725on a lock screen, the same as any other text a phone shows in a
726notification. This is deliberate, not an oversight — weigh it against
727what you keep in a private repository before registering a device.
728
729* Web vocabulary
730
731Sign in / Log out, Search, sentence-case headings and buttons, product
732name lowercase.
733
734* Output rules
735
736=--json= is the contract; the plain output is for a person at a
737terminal, and follows these rules so every noun reads the same way.
738
739- A list command prints one row per item, tab-separated, no header.
740  Columns run identifier, state, then description; a trailing column
741  may carry a word (=due 2027-01-01=, =via team=). Piped, a timestamp in
742  a row is RFC3339 to the second, UTC. Under =--json= nothing is
743  touched.
744- An empty list prints nothing on stdout and =nothing to list= on
745  stderr.
746- A mutation prints one line: verb, object, identifier
747  (=created krz/gitbay#7=). A second line appears only for something
748  to copy: a URL, a token shown once.
749- A bad invocation prints =usage:= and the command's registered usage,
750  the same text =help <noun>= shows. Exit 2.
751- A refusal says who may and what to do instead: =only admins of acme
752  can manage teams; ask one to add you=. Exit 4. A thing that does not
753  exist, or that you may not know exists, is exit 3.
754- stdout is the result; stderr is everything else: progress, a note
755  that output was truncated, errors.
756- Verbs: =create= and =delete= for things with their own identity
757  (repository, issue, release, team), =add= and =remove= for attaching
758  something to them (a key, a member, a label on an issue), =set= for
759  a value, =revoke= for a credential, =show= and =list= for reads.
760
761** At a terminal
762
763The =gitbay= CLI sends a leading =--term=<cols>[,color]= argument on
764the SSH command line when stdout is a terminal (OpenSSH's multiplexed
765sessions, which the CLI uses, do not forward a session's =SetEnv=).
766The argument goes first: the server reads =--term=<v>= only as the
767first argument, and ignores it over HTTP. The server then prints:
768
769- lists under a header, padded, fitted to the width (the title or
770  description column is cut with =…= first), states in colour, ages as
771  =2h ago=, and the next page as a command on stderr (piped output
772  keeps a =next\t<cursor>= row instead);
773- =show= views with a title line, aligned fields, the body rendered
774  from markdown or org, one line per event, and comments under a rule;
775  a sub-table (labels, revisions, and the like) carries a section label
776  line in both terminal and piped output; timestamps as
777  =2026-09-23 23:26 UTC=. =wiki show= piped prints the page source
778  verbatim; the rendered page is terminal-only;
779- help with flag descriptions, defaults, and examples; =gitbay --help=
780  groups commands under WORK, REPOSITORIES, YOU and INSTANCE;
781  =help --json= adds =flags= and =examples=.
782
783=NO_COLOR=, =TERM=dumb= and =--no-color= drop the colour. =show=,
784=diff= and =log= at a terminal go through =$GITBAY_PAGER=, else
785=$PAGER=, else =less= (with =LESS=FRX= when =LESS= is unset); an empty
786=GITBAY_PAGER= turns paging off. Paging never applies to =--follow=,
787=--json=, or piped output.
788
789=GITBAY_TERM=off= stops the CLI sending =--term= at all, for an
790instance older than v1.36.0, which refuses the argument as an unknown
791command.
792
793Stock ssh without the CLI's multiplexing gets the plain output unless
794it passes the same leading argument or sets the environment variable
795sshd is told to accept. OpenSSH parses options after the host, so the
796argument goes after =--=:
797
798#+begin_src sh
799ssh git@gitbay.org -- --term=120,color issue list krz/gitbay
800ssh -o SetEnv=GITBAY_TERM=120,color git@gitbay.org issue list krz/gitbay
801#+end_src
802
803* Scripting
804
805Every read command takes =--json= and emits one envelope:
806={"protocol_version": 1, "data": ...}=. stdout is data, stderr is
807messages. Exit codes are stable: 0 ok, 1 failure, 2 usage, 3 not found,
8084 denied, 5 server/protocol error. Usage means the arguments were wrong;
809a refusal — a name already taken, a state that does not allow the change
810— is a failure. Nothing ever prompts; destructive commands take =--yes=.
811
812For HTTP automation see [[API]].
813
814* CLI setup
815
816#+begin_src sh
817gitbay remote add myforge forge.example.org [--port n] [--user u] --default
818gitbay remote list
819gitbay init [name] [--private]   # git init + repo create + origin, in one step
820#+end_src
821
822Configuration lives at =~/.config/gitbay/config.toml=. The CLI shells
823out to your real =ssh=, so =~/.ssh/config=, the agent, and hardware keys
824all apply. It shares one connection per instance through a control
825socket under =~/.ssh=: the first command in five minutes pays the
826handshake and the rest ride it. =no_multiplex = true= on an instance in
827the config turns that off. Man pages: =gitbay man --dir <dir>=; completions:
828=gitbay completion bash|zsh|fish=.