.gitbay/wiki/Users.org

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

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