CHANGELOG.org
1454 lines · 75145 bytes
1#+title: gitbay changelog
2
3Versioning follows semver from v0.1.0. Database migrations run
4automatically on daemon start; upgrade notes appear per release when
5anything beyond "replace the binary and restart" is needed.
6
7* v1.19.0 — 2026-09-11
8
9Labels and milestones an org defines once for every repository under
10it, and =Closes owner/name#N= acting across repositories (#203).
11
12** Org labels and milestones
13
14- Migration 0052: =labels= and =milestones= rows belong to a
15 repository or to an org, exactly one, with a partial unique index
16 per scope. Existing rows and their memberships keep their ids.
17- A repository owned by an org sees the org's labels and milestones
18 beside its own; =issue label --add=, =issue milestone= and =mr
19 milestone= resolve the org's row first. A repository cannot create,
20 recolour, remove, close or reopen a name its org holds; the refusal
21 names the org command.
22- =org label set|list|remove= and =org milestone
23 create|list|close|reopen=, for org admins. Creating a name that
24 repositories under the org already use folds them in: their issues
25 and merge requests move to the org's row and the repository rows
26 go. =repo transfer= into an org folds the same way. Counts on org
27 rows span the repositories the caller can read.
28- Web: read-only =/<org>/-/labels= and =/<org>/-/milestones=, linked
29 from the org page; an "org" mark on repository label and milestone
30 pages, whose forms act on repository rows only.
31- =label list= and =milestone list= JSON carry =org: true= on an org
32 row.
33
34** Cross-repository closes
35
36- =Closes owner/name#N= in a commit on the default branch, or in a
37 merged merge request's title or body, closes that issue when the
38 actor holds write on the target and the pushing key's scope allows
39 it: a deploy key never acts outside its binding. An unknown or
40 refused target does nothing and logs nothing; the text stays a
41 plain autolink. An archived target is refused. Bare =owner/name#N=
42 without a keyword stays display-only.
43
44** Store
45
46- A migration whose first line is =-- foreign_keys: off= runs on one
47 pinned connection with foreign keys off, re-enabled afterwards and
48 =foreign_key_check= required empty. Rebuilding a table other tables
49 reference loses the children's rows otherwise, even under
50 =legacy_alter_table=.
51
52** Behaviour changes
53
54- =milestone create= exits 1 on a duplicate title, as =org milestone
55 create= does; it exited 2 before.
56
57Upgrade: replace the binary and restart; migration 0052 runs on
58start and copies every label and milestone row once.
59
60* v1.18.1 — 2026-09-11
61
62A runner that cannot hang on a dead connection, SSH keys with names,
63and dependency updates.
64
65** CI runner
66
67- Every ssh invocation carries =ConnectTimeout=10=,
68 =ServerAliveInterval=15= and =ServerAliveCountMax=3=, placed after
69 the identity and the operator's =-ssh-opts= so an operator's value
70 wins. A claim whose TCP session had died under it blocked a laptop
71 runner's poll loop for thirteen hours; it now fails within a minute
72 and the loop resumes.
73- The build comment in =deploy/Containerfile.ci= stages the file in
74 the runner user's home before =podman build=: a login shell under
75 =su= cannot read root's stdin and does not share root's =/tmp=.
76
77** Keys
78
79- SSH keys carry a label (migration 0051). =keys add --label= defaults
80 to the authorized_keys comment; =keys label <fingerprint> [<text>]=
81 renames or clears it. =keys list=, =admin user show=, =repo
82 deploy-key list= and the settings page show it. Deploy, runner and
83 admin-created keys take the comment as their label. Closes #208.
84
85** Web
86
87- Org autolinks stop before trailing punctuation: a bare URL closing a
88 sentence or a parenthesis no longer takes the =.=, =,= or =)= into
89 the href. Closes #209.
90
91** Dependencies
92
93- golang.org/x/crypto 0.57, x/image 0.46, x/net 0.59, x/term 0.46.
94 Closes #205.
95
96* v1.18.0 — 2026-09-10
97
98What the first real job on a user's runner needed. A twice-daily
99archive pull that had run on the server's host moved into a container
100and then onto a laptop, and each hop found a gap.
101
102** CI runner
103
104- A secret with newlines — a private key — reaches the build. An env
105 file has no escape for one, so such values are named on podman's
106 command line with =--env NAME= and valued in the podman process's own
107 environment; the value touches neither argv nor the file.
108- =GITBAY_SSH= in every build's environment: the instance's ssh
109 destination as the build reaches it. Under podman the host's own
110 addresses belong to the container (pasta), so a loopback remote is
111 rewritten to the address pasta exposes the host at. A job that
112 publishes back to the instance uses the variable rather than
113 guessing. Inside a container =ssh= expands =~= from the passwd entry,
114 not =$HOME=; keep a build's ssh config in the workspace and pass =-F=.
115- =-identity= drops the user's =~/.ssh/config= (=-F /dev/null=):
116 =IdentitiesOnly= kept identities the config named, so a laptop runner
117 authenticated as the user's own full-scope key and, on an admin's
118 machine, could claim every repository's builds. A first-seen host key
119 is accepted, since a service cannot answer a prompt.
120- A schedule tick queues nothing while the job's last build is pending
121 or running, matching the push path. A repository no runner served
122 gained one row per tick forever. A finished build does not suppress
123 the tick; a schedule re-runs an unchanged commit on purpose. #206
124- =admin runners forget <fingerprint>= drops a key's heartbeat row; the
125 key stays. =admin runners= rows are the repositories a key may claim.
126- The bay1 unit names no =-repos=: the runner key's attachments are the
127 boundary. =gitbay-ci:2= carries =python3-venv= and =sqlite3=, since
128 it is also the default image for every repository the runner is
129 attached to.
130
131** Decisions
132
133- Claim order stays oldest-first across everything a key may claim; a
134 repository that must never wait on another gets a runner of its own.
135 #207
136- No per-account cap on queued builds and no schedule floor: with the
137 tick dedupe a repository with no runner holds one row per scheduled
138 job, and a busy schedule spends its owner's compute. #206
139- Attaching a runner is =n/a= on iOS: the key lives on the machine that
140 runs builds.
141
142** Documentation
143
144- Users: =GITBAY_SSH=, ssh's home inside a container, the =ci.yml= caps
145 (ten jobs, fifty steps, 4096 bytes a step, 64 KiB a file) and where a
146 broken config shows up. Admin, CI and Threat-Model follow the changes
147 above.
148
149** Upgrading
150
151Replace the binaries and reinstall the CLI. No migration. A runner
152started with =-identity= (every runner =gitbay-runner init= set up) now
153ignores =~/.ssh/config=; a runner that relied on it for a =ProxyJump=
154or an alias passes the equivalent in =-ssh-opts=. Homebrew:
155=brew upgrade krz/tap/gitbay-runner=.
156
157* v1.17.0 — 2026-09-09
158
159CI without the operator's compute: a runner anyone installs and
160attaches to their own repositories, and the server rule that makes
161that safe. The rest of the #184 list besides.
162
163** CI
164
165- A runner key claims builds only for the repositories it is attached
166 to. =repo runner add <owner/name> < key.pub= attaches one; a key the
167 server has not seen lands on the caller's account with scope
168 =runner=. =repo runner list= shows each attached key with its last
169 poll and the build it holds; =repo runner remove <fingerprint>=
170 detaches it. All three on the settings page. A runner-scoped key
171 with nothing attached claims nothing, whoever owns it; a full-scope
172 admin key still claims any repository. =runner log= and =runner
173 done= refuse a build outside the key's attachments. #184
174- Closed: any account could add a runner-scoped key, and =runner next=
175 with no arguments handed it the oldest pending build on the
176 instance, secrets included.
177- =runner next --untrusted=: without it a merge request head from a
178 fork is never claimed. The runner passes it when started with
179 =-untrusted=; the bay1 unit does, because it isolates in podman.
180- =gitbay-runner init= generates a key under =~/.config/gitbay-runner/=,
181 writes =config.toml= beside it, and prints the public key with the
182 command that attaches it. The runner reads that file when present
183 and flags override it. =-identity=, defaulting to the generated key,
184 makes ssh and git use the runner's own key and no other. Homebrew:
185 =brew install krz/tap/gitbay-runner=, then =brew services start
186 krz/tap/gitbay-runner=. Releases ship =gitbay-runner= binaries.
187- =admin runners= heads its output with the queue: builds pending now,
188 and over the last day the builds claimed, the wait to claim (average
189 and worst) and the builds the reaper ended instead of a runner
190 reporting them. Rows are per key, with the fingerprint and the
191 repositories the key may claim. Migration 0049. #184
192- Stop tests for the runner's drain: a signal mid-step, the drop-in's
193 =KillMode= and =TimeoutStopSec=, and a transient systemd user unit
194 where one exists. #179
195- The CI wiki page carries every push shape against every job kind and
196 what dedupe, path filters, schedules and the reaper make of it, with
197 a test per row. #176, #177
198
199** Importing
200
201- =import-issues --api-base= reads Forgejo instances: paging by =limit=,
202 oldest first, comments read once, attribution and pull heads taken
203 from the API base rather than github.com. #191
204
205** Documentation
206
207- Users: "Your own runner". Admin, Threat-Model, CI, FAQ and Parity
208 follow the attachment rule.
209
210** Upgrading
211
212Replace the binaries and reinstall the CLI. Migration 0050 (runner
213attachments; the runner heartbeat table is rekeyed by key and
214emptied), applied on start. One behaviour change: a runner polling
215with a key of scope =runner= claims nothing until a repository admin
216attaches it with =repo runner add=. Do that right after the restart.
217A full-scope admin key is unaffected. Merge request heads from forks
218are built only by a runner started with =-untrusted=.
219
220* v1.16.0 — 2026-09-08
221
222What stops a stranger from moving here: the repository plumbing and
223the team gates the #185 walk found missing, twelve merge requests
224merged as one stack.
225
226** Repositories
227
228- =repo settings default-branch <owner/name> <branch>= moves HEAD and
229 the record; a branch that does not exist is refused. A first push of
230 =master= or =trunk= into a fresh repository moves the unborn HEAD to
231 the branch that arrived, so the clone checks something out. Settings
232 page select. #189
233- =repo rename <owner/name> <new-name>=: the row and the directory move
234 together, with transfer's revert on a failed move. Clone URLs change.
235 #190
236- =repo settings require-mr on=: an existing protected branch refuses
237 every direct push in pre-receive, =repo commit-file= and the web
238 editor included; =mr merge= is its only writer. Creating the branch
239 is still a push. #197
240- =repo settings protect-tag <glob>= (and =unprotect-tag=): matching
241 tags are created once and refuse moves and deletion. Separately, a
242 tag a release is anchored to refuses both while the release exists,
243 protected or not; delete the release first. #201
244- =repo access list= reports effective access: one row per account
245 with the highest role and where it comes from (owner, direct, org
246 admin, org member, team). It used to list direct grants only, so a
247 repository reached through teams answered with nothing. #200
248- Removing an org member drops their team memberships in the same
249 transaction. They kept every grant their teams carried, and re-adding
250 them to the org restored nothing because nothing had gone. #196
251- An outsider gets one answer about an org: its existence and members
252 are public, its teams are for members, and =org team list= and =show=
253 refuse a non-member rather than say the org does not exist. Ref #200
254- A repository name may not end in =.atom=, as it may not end in
255 =.git=. Ref #192
256
257** Merge requests
258
259- A push that leaves the merge request's diff unchanged keeps its fresh
260 reviews: the patch-id of each revision against its own merge base is
261 compared, and a rebase onto a target that moved on no longer stales
262 every approval. A push that changes the diff stales them as before.
263 #198
264- The merge gates are one computation shared by =mr show= (a =gates=
265 block: approvals counted and required, owners outstanding per file,
266 open threads, checks, whether a fast-forward is possible), the merge
267 request page, and =mr merge=, which names every unmet gate in one
268 refusal rather than the first. =mr review= reports =counts= and says
269 when a verdict is advisory because the reviewer cannot write. #199
270
271** Notifications
272
273- =@name= in an issue, merge request, comment or diff comment files
274 "mentioned you" in that account's inbox and makes them a participant
275 of the thread, provided they can read the repository and have not
276 muted it. Watchers are not told about a mention addressed to someone
277 else. Migration 0047. #202
278- =notifications settings mail on|off= (and =show=): inbox rows are
279 filed either way, this is the mail half. Login links are not activity
280 and still arrive. The account page carries the same switch.
281 Migration 0048. Ref #194
282
283** Web
284
285- Atom feeds: =/{owner}/{repo}/releases.atom=,
286 =/{owner}/{repo}/log.atom[/{ref}]= and =/{owner}/activity.atom=,
287 rendered over the rows the pages already serve. The owner feed covers
288 public repositories only, since a reader carries no session; a
289 private repository answers 404. The releases, log and owner pages
290 carry the discovery link. #192
291
292** Documentation
293
294- The Users page says what differs over stock ssh: a multi-word value
295 needs a second layer of quotes, and =help <noun>= replaces =--help=.
296 The companion-wiki paragraph #170 made stale now points at
297 =.gitbay/wiki/=. #204
298
299** Upgrading
300
301Replace the binary and reinstall the CLI. Two migrations (0047, the
302mentions table; 0048, the per-account mail switch), applied on start.
303Nothing changes behaviour until a setting is turned on: =require-mr=,
304=protect-tag= and =mail off= are all opt-in.
305
306* v1.15.0 — 2026-09-07
307
308Builds run in containers, a rebase is not rebuilt, and a runner restart
309no longer strands the build it was running.
310
311** CI
312
313- A build's steps run in a rootless podman container, one per job. The
314 runner clones outside the container with its key and bind-mounts the
315 workspace in, so a step cannot reach the key, the runner's environment,
316 or another build's workspace. =image:= is required per job and is
317 validated as a reference, never pulled: an operator provisions images
318 on the host. A runner configured for podman that has none refuses to
319 start rather than fall back to the host. =-isolation none= is the
320 explicit host-execution mode. =-cpus= and =-memory= cap a build's
321 container. #144
322- Builds have a home directory that outlives the build, so the Go module
323 cache and the sonar scanner survive between runs. It is mounted into
324 the container at the same path, and only that directory. Ref #144
325- The host preparation script, the unit drop-in and the weekly image
326 prune ship with =make deploy-runner=; each hardening flag the drop-in
327 relaxes carries the reason in place. Ref #144
328- A nightly canary on the runner host proves the boundary holds:
329 =cmc/ci-smoke= runs a job that tries to read the key and list sibling
330 workspaces, and fails if either succeeds. Ref #144
331- Dedupe keys on the commit's tree, not its sha. A rebase that changes
332 nothing is not rebuilt: the new commit gets the earlier success as its
333 status, naming the build. Scheduled and tag builds carry no tree and
334 are never reused. #177
335- =vuln= and =sonar= run nightly on =main= rather than on every push;
336 neither could inform a merge. Branches run =build= and =test=. #177
337- A rewritten branch is filtered against the merge base rather than the
338 old tip, so a rebased branch whose own commits change code no longer
339 reads as a docs-only push and skips its jobs. #176
340- A build whose runner's log stream ended with no outcome is failed two
341 minutes later, instead of sitting =running= until a fixed deadline.
342 Migration 0046. Ref #179
343- The runner retries reporting a finished build's outcome, four times
344 over about thirty seconds, when the server is unreachable. A gitbayd
345 restart at that moment used to lose the result. Ref #179
346- The runner drains on SIGTERM: it claims nothing more, finishes and
347 reports the build in flight, then exits. A second signal exits at once.
348 The unit allows fifty minutes. #179
349
350** Repositories
351
352- =repo show= reports the caller's watch and bookmark state, and the
353 parent of a fork when the caller can read it. #178
354- =repo unwatch= clears the row from either state; =repo mute= is the
355 explicit silence. Watch then unwatch used to leave an account below
356 the default with no way back. The web header toggle follows. #180
357
358** Upgrading
359
360Replace the binary and reinstall the CLI. One migration (0046, the
361build log's close time), applied on start.
362
363The runner now defaults to =-isolation podman= and refuses to start
364without a working podman and a named image. Prepare the host with
365=deploy/runner-podman-setup.sh= and build =localhost/gitbay-ci:1= from
366=deploy/Containerfile.ci= before deploying it, or pass =-isolation
367none= to keep running steps on the host. Validate on a scratch
368repository with =-repos= scoped to it first; the wiki's Admin page has
369the procedure.
370
371The runner's unit gains =TimeoutStopSec=50min= and =KillMode=mixed= so
372a stop reaches the runner alone and it can drain; under the default
373kill mode systemd ends the build's container with the runner.
374=make deploy-runner= therefore waits for a build in flight.
375
376* v1.14.0 — 2026-09-06
377
378Logging into the web without an SSH key, wikis inside the repository,
379CI that skips what a change does not touch, and the browser catching up
380with the command line on nine capabilities.
381
382** Accounts and the web
383
384- A browser session can be requested by mail: the login page takes a
385 username or a verified address and sends a link that works once and
386 expires in fifteen minutes. An account with no SSH key had no way into
387 the web at all. The response never says whether the account exists, and
388 the form appears only when the instance has SMTP. #155
389- A login link resolves to any verified address on the account, not only
390 the primary. #158
391- Login mail is queued rather than sent from a goroutine, so a link in
392 flight survives a restart. #159
393- A suspended account cannot mail itself a session, and a link minted
394 before suspension opens nothing. #156
395- The session cookie is =SameSite=Lax=, and a test now walks every
396 mutating route to assert it carries the origin check. #157
397- =POST /register= is reachable on an open instance again.
398
399** Wikis
400
401- A wiki lives at =.gitbay/wiki/= on the default branch, so editing one
402 is editing a file in the repository — a push, or the web editor — and
403 it is cloned, diffed and reviewed like anything else. The companion
404 =<repo>.wiki= is gone. #170
405
406** CI
407
408- Path filters: =paths= and =paths-ignore= per job, so a docs commit
409 does not run the whole suite. #169
410- A branch's first push derives its diff base from the merge base
411 rather than treating every file as changed. #171
412- A job filtered out records a skipped status instead of nothing, so
413 =require_checks= cannot wait forever for a check that will never
414 arrive. #172
415- A force-push no longer leaves queued builds pointing at an object
416 that is gone; the orphan is cancelled when a runner claims it. #152
417- A build step's environment is constructed rather than inherited. It
418 was =os.Environ()= plus the build's variables, which handed repository
419 content everything set on the runner service. A step now gets =PATH=,
420 =HOME=, =LANG=, =CI=, its =GITBAY_*= variables and its secrets. =HOME=
421 is the workspace, so a build cannot read the runner's =.netrc=,
422 =.npmrc= or =.gitconfig=, where tools keep credentials. Ref #144
423- A runner host can be prepared for rootless podman:
424 =deploy/runner-podman-setup.sh=, the systemd changes it needs, and
425 weekly image pruning. Nothing reads them yet; the preparation ships
426 before the runner that requires it, on purpose. Ref #144
427
428** Collaboration
429
430- =mr review request --add <user>= asks a particular person for a
431 review, who then carries it in their queue and is notified;
432 =--remove= withdraws the ask. The queue was computed from involvement
433 alone, so a collaborator who had not touched a thread heard nothing.
434 #145
435- Bookmarks save someone else's repository to come back to, and the
436 count is the instance's only popularity signal. Separate from pins
437 rather than a flag on them: a pin is private quick access to your own
438 work and drives the rail, a bookmark is public and counted. =repo
439 bookmark=, =repo unbookmark=, =repo bookmarks=, a control on the
440 repository header, the count in the facts bar, and =/bookmarks=. Read
441 access is all a bookmark needs, and one made while a repository was
442 public drops out of the listing if it goes private. #146
443
444** The browser catches up
445
446Nine capabilities that were CLI-only and had no reason to be:
447
448- label management — list, create, colour, remove #163
449- dependency status under the toggle that turns checks on #164
450- release delete #165
451- the account export bundle, as a download #166
452- organization create and rename; delete stays CLI-only, wanting a
453 typed confirmation, and the page says so #167
454- opening a merge request from a fork, with a source picker offering
455 the branches of every fork you can push to #168
456- forking a repository #174
457- cancelling a queued or running build #162
458- the profile form #161, and a markup picker on issue and merge
459 request create #160
460- admin table headers align with their columns #151
461
462** CLI
463
464- =gitbay mr rebase <n>= replays a merge request's branch onto its
465 target and re-pushes it. A repository requiring signed commits accepts
466 only fast-forward merges, so a merge request whose target moved had to
467 be rebased by hand; the refusal already named the procedure and this
468 runs it. The git work is local, so the replayed commits carry your
469 signature — the server still holds no key. A branch in a fork is
470 refused, naming the repository to run it in. #175
471
472** Security
473
474- Mutating commands are bounded per account in the dispatcher, so SSH,
475 the JSON API and the web spend one budget and a caller cannot refresh
476 it by changing surface. Authentication failures were throttled;
477 commands were not. #148
478- A dead-lettered mail is logged by its queue row id, not the recipient
479 address, and the relay's error is redacted before logging because a
480 rejection quotes the address it rejected. The address stays on the
481 row, where =/admin= shows it. One rule for every mail type. #173
482- The 2026-09 sweep's coverage — and what it did not reach — is recorded
483 in the wiki's Threat-Model rather than in a closed issue. #149
484
485** Dependencies
486
487- goldmark 1.8.6, sqlite 1.58.0. #140
488
489** Upgrading
490
491Replace the binary and reinstall the CLI. One migration (0043,
492bookmarks), applied on start.
493
494=limits.write_rate= defaults to 60 mutating commands a minute per
495account. A script that writes faster will be refused with exit 4 and a
496retry time; raise it in =[limits]=, or slow the script. Read-only
497commands, the runner protocol and the host CLI are not counted.
498
499A companion =<repo>.wiki= repository is no longer read. Move its pages
500to =.gitbay/wiki/= on the default branch.
501
502Runner isolation is half done. Builds still run as the runner's user
503with no container: the step environment no longer leaks the service's,
504and a host can be prepared for podman, but nothing yet executes a build
505in one. Do not enable CI for repositories you do not trust. #144 stays
506open for the execution half.
507
508* v1.13.3 — 2026-09-05
509
510Only a writer's review decides a merge gate, and a scan of one branch
511stops overwriting another's results.
512
513- =mr review= resolves with =CanRead= and applied no further check, and
514 the merge gates counted every fresh verdict. On a public repository
515 that let anyone with an account satisfy =require_approvals= — defeating
516 four-eyes review by holding two accounts, which on an instance with
517 open registration means defeating it outright — and equally let a
518 passer-by block a merge the owner wanted, with no override and only a
519 force-push to clear it. Both were confirmed against a running instance.
520 Reviewing stays open to every reader, because an outside opinion on a
521 public change is worth having; it no longer decides the gate. What
522 does is write access, the same question the gates already answer. An
523 uncounted review is marked advisory by =mr show= and by the merge
524 request page, both reading the same rule, so the page cannot show an
525 approval the gate ignores. #147
526- The two =git upload-pack= spawns behind the smart HTTP transport now
527 resolve git at start-up like everything else. The v1.13.2 rewrite
528 matched a plain identifier for the context and these pass
529 =r.Context()=, so it skipped them. #153
530- CI analyses each branch as itself. Every scan was recorded against the
531 project's main branch whatever branch produced it, so a feature branch
532 replaced main's results — a branch that removes findings made main look
533 clean before the fix merged, and one that added them made main look
534 broken when it was not. #154
535
536Replace the binary and reinstall the CLI. No migration.
537
538A repository relying on approvals from accounts without write access
539will find those merges now refused. That was the defect: such an approval
540never meant the repository accepted the change. Grant write, or name the
541reviewer in CODEOWNERS and grant write, if their approval is meant to
542count.
543
544* v1.13.2 — 2026-09-04
545
546The first SonarCloud scan's findings: eight fixed, the rest triaged and
547dismissed with reasons.
548
549- A pages directory redirect could leave the site. Both redirects passed
550 the raw request path to =http.Redirect= while the cleaned path sat a
551 line above; =net/url= keeps a leading =//=, and Go emits
552 =Location: //evil.example/= unchanged, which a browser reads as
553 protocol-relative. Reaching it needed a public repository named like a
554 host under the subdomain's own owner — repository names permit dots —
555 so it was narrow rather than impossible. The destination is built from
556 the cleaned path through =url.URL= now, which also settles the two
557 spellings the first fix missed: a =..= that escapes to the root, and a
558 backslash, which browsers following the WHATWG rules treat as a
559 separator. #153
560- The runner's default workspace was =<tmp>/gitbay-runner=: a fixed name
561 in a world-writable directory, created with =MkdirAll=, which succeeds
562 against a directory whoever already owns it. bay1 was never exposed —
563 its unit names a workspace — but the default is what anyone running the
564 binary by hand gets, and this is the process that clones repositories
565 and exports build secrets. The default moves under the user's cache
566 directory, the workspace is created 0700, and a symlink or a directory
567 owned by someone else is refused. One we own that is merely too
568 permissive is tightened rather than refused, since every runner before
569 this one made it 0755 and refusing would take the runner down on
570 upgrade. #153
571- Logout and flash consumption expire their cookies with the attributes
572 the setting calls used. Deletion worked either way; the difference was
573 a reviewer's puzzle, and four findings. #153
574- The topics-remove field has a label. A placeholder is not an accessible
575 name. TestEveryInputHasAnAccessibleName is the guard that was missing,
576 and it knows both associations — six inputs use
577 =<label>Name <input></label>=, which is as good as for/id. #153
578- git and ssh are resolved once at start-up rather than searched on every
579 spawn, and gitbayd refuses to start when one is absent instead of
580 failing on whichever request first needed it. This was 74 of the 118
581 findings; a dashboard that is mostly permanent noise is one nobody
582 reads. #153
583
584Also: SQLite migrations are excluded from analysis. They were read as
585PL/SQL, where ='' is NULL=, so =WHERE col = ''= on a =NOT NULL DEFAULT ''=
586column — correct SQLite, and the shape used throughout — read as a
587null-comparison bug.
588
589Replace the binary and reinstall the CLI. No migration. A runner whose
590workspace is group- or world-readable will have it tightened to 0700 on
591next start, and will refuse to start if that workspace is a symlink or
592belongs to another user.
593
594* v1.13.1 — 2026-09-04
595
596A command that reads its payload from stdin says so, and SonarCloud runs
597alongside the vulnerability scan.
598
599- =repo secret set= blocked with nothing printed, which is
600 indistinguishable from a hung connection, and pressing Enter did not
601 end it because the server reads to EOF — so it looked the same before
602 and after the value had been typed, and the secret echoed into the
603 scrollback on the way. A terminal is now told what is wanted and that
604 Ctrl-D ends it; a secret is read without echo and takes one line, so
605 Enter is enough. Piped input is unchanged, byte for byte: =printf %s
606 "$TOKEN" | gitbay repo secret set ...= still sends exactly the token.
607 Applies to =keys add=, =auth pgp add=, =repo deploy-key add= and
608 =release asset add= as well; public keys keep echoing, since they are
609 public. #150
610- A =sonar= CI job reports to SonarCloud, alongside =vuln=. Report-only:
611 it does not gate a build, unlike the vulnerability scan. A merge
612 request from a fork builds without secrets by design, so the job says
613 why it is skipping there rather than failing on a missing credential.
614 Only =SONAR_TOKEN= is secret and it is a build secret; the
615 organization, project key and host are checked in. Ref #149
616
617Replace the binary and reinstall the CLI. No migration.
618
619* v1.13.0 — 2026-09-04
620
621Collaboration. A review is composed and submitted as one thing, a merge
622request can say it is not asking yet and can show what changed since you
623last looked, issues are searched by their text, and the events half the
624mutations never recorded now exist.
625
626- A review is composed as a unit. =mr diff-comment --pending= holds a
627 comment back; =mr review= publishes the batch with the verdict and says
628 how many went with it; =mr review --discard= throws away what was never
629 submitted. A pending comment notifies nobody when written — the review
630 is the announcement, and it names its own size — and does not gate a
631 merge, since a thread only its author can see is one nobody else could
632 resolve. #111
633- =mr create --draft=, =mr draft=, =mr ready=. A draft does not merge and
634 does not appear in anyone's review queue; marking it ready is the
635 request for review. Draft is a flag rather than a fifth state, so every
636 =state = 'open'= rule still means what it did, and the merge refusal is
637 unconditional: every other gate is a setting an admin turns on, and
638 this one is the author's own statement about their own work. #111
639- =mr range-diff= shows what changed between two revisions of a merge
640 request, and =mr revisions= lists them. A push stales every review and
641 nothing said what had moved; a diff of the two heads cannot answer it,
642 and the previous head was overwritten in place. Each side of the
643 comparison carries the merge base it had at the time, since measuring
644 both against today's would attribute every commit that landed on the
645 target in between to this merge request's author. #111
646- Issues and merge requests are searched by their title and body, over
647 FTS5. =issue list --search= and =mr list --search= narrow one
648 repository; the instance-wide =search= reaches bodies now, and both
649 list pages carry a search box. What someone types is quoted term by
650 term, so an FTS5 operator — =c++=, =AND=, a lone quote — is a word to
651 match rather than a syntax error. #114
652- Ten mutations that changed a repository silently now record an event:
653 =mr.closed=, =mr.reviewed=, =mr.edited=, =mr.retargeted=, =mr.draft=,
654 =issue.edited=, =issue.labeled=, =issue.assigned=, =issue.milestoned=,
655 =mr.milestoned=, =release.deleted=. =mr close= and =issue edit= also
656 notify participants, which they did not. The full list is on the API
657 wiki page and =webhook add --events= refuses a name that is not on it,
658 since a subscription to a typo would never fire and nothing would say
659 so. #112
660- =gitbay-runner -jobs N= runs N builds at once. Claiming was always a
661 single transaction that selects and updates, and each build already
662 worked in its own directory, so several workers were safe the whole
663 time; the runner never used more than one. #115
664- A merge request replayed from a migration bundle has a diff. The bundle
665 carried no head at all, and =mr diff= resolves through the head ref, so
666 a merged merge request — which has no source branch left — could only
667 fail. Re-running an import after the git push sets the head it could
668 not set the first time. The GitHub import fetches =refs/pull/*/head=,
669 which a mirror made with the default refspecs does not carry. #128
670- The review loop is driven end to end by three accounts in the test
671 suite: draft, ready, thread, approval, resolve, merge, with approvals,
672 CODEOWNERS and require-resolved all on at once. Every one of those had
673 its own test and none of them met. #139
674- A bare =go test ./...= says the timeout is too short instead of
675 panicking eleven minutes in and blaming whichever test was running.
676 =make test= is what CONTRIBUTING asks for now. #143
677
678Migrations 0036 through 0039 add the search index, the draft flag,
679pending review comments and merge request head history; 0036 and 0039
680backfill from what is already there. They run on daemon start.
681
682Two things this does not do. There is still no way to ask a particular
683person for a review — the queue is computed from involvement, so a
684collaborator with write access who has not touched a thread hears nothing
685(krz/gitbay#145). And a CI build still runs as the runner's own user with
686no container; the runner on this instance is scoped to one repository,
687which is what keeps that narrow (krz/gitbay#144). Both are v1.14.0.
688
689* v1.12.0 — 2026-09-04
690
691The store and the push path. Dashboard queries walk an index instead of
692sorting the table, concurrent writers wait instead of failing, expired
693rows are swept, and a large first push no longer forks a process per
694commit.
695
696- Every dashboard list scanned its table and sorted the result to take
697 fifty rows. Reachability is correlated subqueries and cannot be
698 indexed; the index supplies the =ORDER BY= instead, so the walk stops
699 at =LIMIT=. On 20k issues and 20k merge requests: open issues 11.8ms
700 to 0.8ms, open merge requests 12.1ms to 0.8ms, the review queue 15.5ms
701 to 0.3ms, assigned issues 10.6ms to 0.04ms. The last of those also
702 drives from =issue_assignees= rather than testing =EXISTS= against
703 every issue. #137
704- Concurrent writers serialise rather than failing. Every transaction in
705 the store writes, and a deferred one takes the write lock at its first
706 write, by which point another may hold it; SQLite answers
707 =SQLITE_BUSY= and does not run the busy handler for that case, so
708 =busy_timeout= could not help. Measured with eight concurrent
709 read-then-write transactions, 44% of them failed. Beginning immediate
710 takes the lock up front, where the timeout applies. #121
711- =repo settings= updates no longer overwrite each other.
712 =settings_json= is one blob, and every caller read it off a repository
713 loaded earlier, changed a field and wrote it all back; two admins at
714 once and the later write put back what it had read for the other's
715 field. The read and the write happen together now, under one
716 immediate transaction. #123
717- Expired sessions and tokens are swept, and =[retention]= caps how long
718 the audit log, the activity feed, webhook deliveries and the mail
719 queue keep a row. Unset means forever, which is what every existing
720 instance gets. #122
721- The audit entry records flag names without their values. Secrets never
722 reached argv — they travel on stdin — but prose did: =--body <the
723 whole issue>= was stored verbatim, in a table nothing pruned, for a
724 repository that may be private. Identifiers are positional and
725 survive. #122
726- pre-receive on a require-signed repository forked a =cat-file= per
727 incoming commit and built one JSON message holding the whole push. A
728 50k-commit first push forked 50k processes and held the history in
729 memory twice. One =cat-file --batch= serves the push now, and the
730 daemon verifies each commit as it arrives. #100
731- The payloads other surfaces decode are named types in =control=. httpd
732 had its own copies of what the build, profile and dashboard commands
733 emit, so a field added to a command was silently absent on the page.
734 #126
735
736Migration 0035 adds three indexes; it runs on daemon start.
737
738Two changes in behaviour rather than configuration. A heavily contended
739write now blocks for up to =busy_timeout= (five seconds) instead of
740returning an error immediately — waiting is the point, but a caller that
741treated the error as normal will see a pause instead. And anything
742parsing =audit --json= for command arguments will find flag values gone;
743the flag names and the positional arguments are still there.
744
745* v1.11.0 — 2026-09-04
746
747The web catches up with the rest of the forge: search across the
748instance, a notification inbox, a compare view, linkable diff lines,
749and a pass over contrast, heading structure and the stylesheet.
750
751- Notifications are no longer mail or nothing. An =inbox= table holds a
752 row per recipient whether or not the instance has SMTP;
753 =notifications list= reads it, unread by default, and =notifications
754 read <id>... | --all= clears it. =repo watch= adds you to a
755 repository's notifications and =repo unwatch= mutes it, with a mute
756 beating every other reason to be told, including owning the
757 repository or having written the thread. The unread count rides on
758 =dashboard= and in the web rail. #113
759- =search= matches repository paths, descriptions and topics, and issue
760 and merge request titles, across everything the caller can read;
761 =/search= renders it with a field in the rail on every page, and an
762 anonymous visitor gets the public rows from the same query. Titles
763 only — body search is #114. #118
764- =/{owner}/{repo}/compare/{a}...{b}= shows what one ref adds on top of
765 another. Diff rows, files and review threads carry ids, so a line can
766 be linked to. The issue and merge request lists page at fifty with
767 the cursors the commands already emit. #118
768- CODEOWNERS gating is a setting rather than a file that happens to
769 exist: =repo settings require-codeowners <owner/name> on|off=, off by
770 default, still independent of =require-approvals=. A repository can
771 carry the file as documentation of who to ask without it gating
772 merges. #142
773- A failed form action's reason rides a one-shot cookie instead of
774 =?e=<message>=, so it no longer survives a reload or lands in
775 history. #119
776- A diff cut at 4 MiB cuts on a line boundary and says so, the
777 diffstat says its counts are partial, and a merge request's commit
778 list says "first 100 of N". #117
779- Page templates parse once with the layout at start-up, so a template
780 that does not parse fails the process rather than the first visit to
781 its page. #116
782- Markdown headings carry ids, matching org pages, and the stylesheet
783 serves a hash ETag with a day's lifetime and answers a matching
784 =If-None-Match= with 304. #132
785- Dark-mode buttons carry a visible edge (a 1 px border was 1.3:1
786 against the 3:1 non-text contrast floor), and a stored label colour
787 is held between 0.12 and 0.28 relative luminance so it clears 3:1 on
788 both grounds while keeping its hue. #120
789- Accessibility: the pin star is decoration and the word carries the
790 meaning, the refs and release asset tables have scoped column
791 headers, state filters carry =aria-current= rather than marking the
792 active one by colour alone, and a rendered README or wiki page's
793 headings move down one level so they sit under the page's own. #133
794- The stylesheet drops rules no template reaches, folds two off-scale
795 values onto the type scale, and resolves the classes templates asked
796 for and it never defined. #131
797- On a phone the tab bar wraps to a second row instead of scrolling
798 sideways with its scrollbar hidden, and the landing page says in
799 three sections what a reader, a writer and a reviewer can do. #134
800
801Migration 0034 adds =inbox= and =repo_watchers=; it runs on daemon
802start. Reinstall the CLI for =search=, =notifications= and =repo
803watch/unwatch=.
804
805Two upgrade notes beyond replacing the binary. A repository that
806relied on a CODEOWNERS file gating merges by its presence alone loses
807that gate until =require-codeowners= is set: nothing back-fills it,
808since the database cannot say which branches carry the file. And
809=search= and =notifications= are now reserved top-level routes, so a
810user or organization already holding either name keeps it but its
811profile page is shadowed by the route.
812
813* v1.10.0 — 2026-09-04
814
815Every command parses its arguments the same way, the web answers by
816exit code, and a restart no longer waits on idle connections.
817
818- One flag parser for every command: an unknown flag, a missing value or
819 too many arguments is exit 2 with the command's usage, everywhere.
820 =mr review --approve --bogus= used to report repository =--bogus= as
821 not found. #96
822- =--json= is honoured by the dispatcher's own refusals, so a script told
823 no gets the envelope. #109
824- A store failure is exit 1 and not-found is exit 3; both used to be
825 usage errors. #107
826- Web form actions answer by exit code: the 404 page for a thing that
827 does not exist, the page with the message for anything else. #106
828- Inside a clone, =gitbay mr create= takes the checked-out branch as
829 =--source=; =gitbay <noun> --help= is the server's reference for the
830 noun, flags included. #101 #130
831- Shutdown closes idle SSH connections at once and drains only sessions
832 mid-command; a store failure during key lookup no longer counts as a
833 bad key against the auth limiter; the CLI says on ssh's exit 255 that
834 a burst may have tripped it. #141
835- A merge request head the target already contains is recorded as
836 merged instead of refused; a fork is created with its parent in one
837 insert; a failed transfer revert is reported; the repository quota is
838 checked under a lock. #108
839- Issues and merge requests share their comment, reference and
840 author-or-write code. #110
841- The payloads the web and clients decode are named types: =Created=,
842 =MRCreated=, =IssueShow=, =MRShow=. JSON unchanged. #126
843- Tests: parse failures per command, non-ASCII paths (which found
844 =ls-tree= returning octal escapes, fixed with =core.quotepath=off= for
845 every git the server runs), concurrent pushes. #129
846
847Replace the binary and restart, and reinstall the CLI. No migration.
848
849* v1.9.0 — 2026-09-03
850
851The runner is no longer an admin, the web no longer reaches around the
852registry, and the CLI stops paying a handshake per command.
853
854- =keys add --scope runner= confines a key to =runner next/log/done= and
855 read-only git; the runner commands accept that scope or an admin. The
856 systemd drop-in sandboxes the runner process. gitbay.org's runner now
857 polls as a non-admin =ci= account scoped to one repository. #92
858- The web's repo create, issue create and edit, MR edit and both comment
859 forms dispatch the command the CLI runs, so the repository quota, the
860 archived-repository refusal, notifications, the body format and the
861 audit entry hold from a browser. #93
862- The CLI shares one SSH connection per instance (=ControlMaster=, five
863 minutes idle): a command costs a round trip instead of a handshake,
864 0.4 s instead of 4 s from a distant laptop. =no_multiplex = true= on
865 an instance opts out. #94
866- A disabled account is refused on every surface, and disabling revokes
867 its API tokens along with its sessions. #95
868- CODEOWNERS gates whenever the file exists on the target branch, not
869 only under =require-approvals=. #99
870- The HTTP listeners have header and idle timeouts, and SIGTERM drains
871 in-flight requests and SSH sessions before exit. #104 #105
872- =git archive= runs under a two-minute deadline and a 512 MiB cap. #124
873- Every git invocation ends option parsing before the ref, so a ref
874 shaped like an option is a bad revision and never a flag. #135
875- The admin noun is gated in the dispatcher as well as in each handler;
876 registry tests cover that and =ReadsStdin=. #127
877- An e2e test runs every =ReadOnly= command against a populated instance
878 and fails on any row it changes. #97
879- =http.trusted_proxies= attributes proxied API requests to the last
880 untrusted =X-Forwarded-For= hop for rate limiting; =email add= is
881 capped at five codes an hour per account. #136
882- A merge request head is built in the target repository, at
883 =refs/merge-requests/<n>/head=, so a fork's merge request has
884 =ci/<job>= statuses for =require-checks= to gate on. A head from
885 another repository is built without the target's secrets. #98
886- Triggers refuse deleting a user or organization that still owns
887 repositories, whatever path the delete takes. #136
888- The stylesheet's fonts are served again, and directory crumbs on blob
889 and blame pages link to the tree. #102 #103
890- The Threat-Model wiki page covers the runner. #138
891
892Replace the binary and restart, reinstall the CLI, and =make
893deploy-runner=: the runner fetches merge request refs before checkout.
894Migrations 0032 and 0033 run on start. The daemon host needs git 2.24
895or newer for =--end-of-options=.
896Operators running =gitbay-runner=: give it a non-admin account with a
897key added by =keys add --scope runner=, remove the admin key it held,
898and =make deploy-runner= to install the sandboxed unit drop-in; an
899admin key keeps working meanwhile.
900
901* v1.8.1 — 2026-09-03
902
903Markdown and org files render when opened.
904
905- A =.md=, =.markdown= or =.org= file renders on its page the way a
906 README does on the directory page, through the same renderer with
907 relative links resolved against the file's directory. =source= in the
908 action bar, or =?view=source=, shows the text as before. Every other
909 file is unchanged. #88
910- The e2e harness waits for the HTTP and git listeners as well as SSH
911 before a test starts; a test whose first act was an HTTP request could
912 be refused, which failed two otherwise green runs. #91
913
914Replace the binary and restart. No migration.
915
916* v1.8.0 — 2026-09-03
917
918The tracker's lists narrow, labels have colours you set, and the CLI's
919help is the server's.
920
921- =issue list= takes =--label=, =--assignee=, =--author= and
922 =--milestone= (a title, or =none= for issues with no milestone);
923 =mr list= takes =--author= and =--milestone=. The store narrows in
924 SQL, and the web lists take the same names as query parameters and
925 show each active filter with a link that drops it and keeps the rest.
926 Both lists had taken =--state= and nothing else, so the web's
927 filtering could never match the CLI's. #84
928- =label list= shows a repository's labels with their colour and how
929 many issues carry each; =label set <label> [--color rrggbb|'']=
930 creates a label or sets its colour; =label remove= takes it off every
931 issue. The colour column had existed since labels did and nothing
932 wrote it. The hash is optional because over bare ssh a bare =#= starts
933 a comment for the tokenizer. =issue label --add= still creates a
934 colourless label on the fly. #85
935- =gitbay help= with a prefix or =--json= runs the server's help, the
936 only place flags are written down and whose JSON is the contract;
937 bare =gitbay help= is still the local tree. Cobra's built-in help had
938 owned the word, so =gitbay help --json= failed on an unknown flag. #86
939
940Replace the binary and restart, and reinstall the CLI for the help
941change. No migration.
942
943* v1.7.0 — 2026-09-02
944
945What an open instance needs before it is open: a cap on what one
946account can take, a way out of a browser session you no longer hold,
947and accounts that never verified do not stay forever.
948
949- =limits.max_repos_per_user= caps the repositories an account owns
950 directly; =repo create=, =fork= and =import= refuse past it with the
951 numbers. =limits.max_bytes_per_user= caps their disk: a push may be no
952 larger than what the owner has left, riding the same
953 =receive.maxInputSize= as the pack cap, and is refused outright when
954 nothing is left. Organizations are not capped. Migration 0031 adds
955 per-account overrides, set with =admin user limits <name> [--repos
956 n|default] [--bytes n|default]= and shown by =admin user show=. Both
957 default to 0, unlimited, so nothing changes until set. #82
958- =registration.pending_expiry=, a duration, removes self-registered
959 accounts still unverified after that long, hourly and once at start,
960 audited as =pending.expired=. Empty keeps them, as before. #82
961- =web sessions list= shows each unexpired browser session by a short id
962 with its creation and expiry; =web sessions revoke <id>= ends one and
963 =--all= ends every one. SSH-only, matching the token commands, and
964 scoped to the caller. #83
965- A step runs in its own process group and a cancel or timeout kills the
966 group. On a host whose =/bin/sh= is dash, killing the shell alone left
967 its child holding the log pipe, so a cancelled build held the runner
968 until the child finished on its own, minutes for a test suite. The
969 wait after a kill is capped at ten seconds besides.
970- A commit with a build still queued or running is not queued again
971 when a fast-forward lands it; the v1.6.0 skip counted only a finished
972 success. #90
973
974- =golang.org/x/crypto= v0.56.0: GO-2026-6354 and GO-2026-6355 in its
975 =ssh= package, both reached through =ssh.NewServerConn= in the
976 embedded SSH server; one is a denial of service on a deadlocked
977 undecided channel.
978
979Replace both binaries and restart: =make deploy= for the daemon
980(migration 0031 adds two columns to =users= on start), then
981=make deploy-runner= for the process-group kill.
982
983* v1.6.1 — 2026-09-02
984
985A cancel reaches a build in its clone.
986
987- The runner honoured a cancel only in its step loop; a build cancelled
988 during its clone or checkout ran git to completion first, which on a
989 loaded host was long enough for the v1.6.0 test to fail on the CI
990 host while passing locally. One =runStep= now drives the clone, the
991 checkout and each step with the cancel signal and the deadline, and
992 checks the signal before starting at all. The runner logs how its log
993 session ended when that was not a cancel.
994
995Replace the runner: =make deploy-runner=. The daemon is unchanged from
996v1.6.0.
997
998* v1.6.0 — 2026-09-02
999
1000The v1.4.0 admin surface answered questions about accounts. This one
1001answers questions about the machinery: which runner is alive, what is
1002stuck, whether last night's backup exists, and it lets an operator stop
1003a build instead of waiting it out.
1004
1005- A build a runner claimed and never reported is failed by the
1006 scheduler's minute tick, with no runner involved. It used to happen
1007 only when some runner polled again, so a dead sole runner left its
1008 builds running forever. =GITBAY_STALE_BUILD_DEADLINE= shortens the
1009 deadline for tests. #75
1010- Runners are visible. Migration 0030 adds =runner_seen=: one row per
1011 runner account with its last poll, the =-repos= scope it asked for,
1012 and the build it holds. =admin runners= lists them, over SSH and on
1013 the host. #76
1014- =GET /healthz=: whether the database answers and which commit serves,
1015 no auth, no repository data, 503 when the database does not answer.
1016 =healthz= joins the reserved names. #77
1017- The host monitor's hourly reading carries the age of the newest full
1018 backup and database snapshot, alerting at 25 and 2 hours, and the
1019 daemon's own =/healthz= answer read through =site_url=. Both ride the
1020 journald line and the webhook body. #78
1021- =admin stats= reports =lfs_bytes=. =admin gc --lfs= scans every
1022 repository's objects for LFS pointers and removes stored objects none
1023 names, keeping anything younger than a day since git lfs uploads
1024 before it pushes. #79
1025- =admin backup --verify <archive>= reads an archive back: SQLite's
1026 integrity check on the snapshot, and the repositories it names against
1027 the archive's. A database-only archive says so. Damage or a missing
1028 repository exits non-zero. #80
1029- A commit that already passed a job on another branch is not built
1030 again when a fast-forward lands it. Merging the six-layer v1.6.0 stack
1031 had queued eighteen jobs for commits that had each just gone green.
1032 Failed or abandoned builds still re-queue. #90
1033- =build cancel <owner/name> <n>= withdraws a queued build or ends a
1034 running one. For a running build the server closes the runner's log
1035 session, the runner kills the step within seconds, and its late report
1036 changes nothing. The commit status goes back to the passed result from
1037 another build of the same commit when one exists, else reads
1038 =cancelled=. Needs write access.
1039- Deploys copy with =rsync --partial=, so a transfer the host's load
1040 stalled resumes, and =make deploy-runner= installs a systemd drop-in
1041 (=Nice=10=, =CPUWeight=30=, =IOWeight=30=) so a build never starves the
1042 host's sshd again.
1043
1044Replace both binaries and restart: =make deploy= for the daemon, then
1045=make deploy-runner= for the runner, which carries its half of running-
1046build cancellation and installs the drop-in. Migration 0030 adds the
1047=runner_seen= table on start. The monitor change ships in
1048=deploy/cloud-init.yaml=; an existing host takes it by hand.
1049
1050* v1.5.0 — 2026-09-02
1051
1052Merge requests stack.
1053
1054- A merge request whose target is the source branch of another open
1055 merge request in the same repository is stacked on it. Nothing is
1056 stored and no flag exists; the forge reads the stack from the
1057 branches. =mr create= says =stacked on !A= when it applies, =mr show=
1058 carries =stacked_on= and =stacked=, =mr list= rows carry =stacked_on=,
1059 and the page says both directions in the header. #87
1060- When the lower merge request merges, everything stacked on it is
1061 retargeted onto what it merged into, with a system comment naming the
1062 merge and its reviews kept: after a fast-forward or merge commit the
1063 diff against the new target is the diff they were of. A squash or
1064 rebase merge under a stack is refused, naming the stack, since it
1065 would rewrite the commits the stack builds on.
1066- The first stack merged on gitbay.org was the six merge requests of
1067 v1.6.0, bottom-up, with no =mr retarget= typed. The wiki's
1068 Stacked-MRs page walks through it.
1069
1070Replace the binary and restart. No migration.
1071
1072* v1.4.0 — 2026-09-01
1073
1074Until now the instance had one operator surface: root on the host. Every
1075question about the instance itself — who is on it, what is stuck, what a
1076spam repository is doing — needed a shell on port 2222. This release
1077gives an instance admin the same answers over SSH, and one page on the
1078web.
1079
1080- =admin user list= pages accounts by username with
1081 =--state active|pending|disabled|admin=, each row carrying its state
1082 and =last_seen=, the newest use of any of its SSH keys or API tokens.
1083 =admin user show <name>= adds the keys with their last use, each
1084 address with how it was verified, PGP keys, org roles, owned
1085 repository count, API token names and live browser sessions. Both are
1086 SSH-only and refused to non-admins, like =audit=. #69
1087- =admin user promote|demote=. The only way to admin was
1088 =admin user create --admin=; an existing account could not be raised
1089 and an admin could not step down. Demoting the last admin is refused
1090 inside the transaction that counts them. The host-local twin is the
1091 recovery path when no admin key is reachable. #70
1092- Repository overrides for moderation: =admin repo list= with size and
1093 last push, and =admin repo archive|unarchive|visibility|delete=. Policy
1094 still knows nothing about instance admin — a private repository
1095 answers not-found to an admin as before — so each override skips the
1096 access check explicitly and writes its own =admin repo.<action>= audit
1097 row. #71
1098- =gitbayd admin= dispatches into the registry. User create, disable,
1099 enable and delete, email verify, invite and stats were reimplemented on
1100 the host; they now live in the registry, SSH-only and admin-gated, and
1101 the host binary runs them as the host — an admin context with no
1102 account behind it, so its audit rows say =source: host= where a
1103 session says the key fingerprint. Invite and stats gain an SSH twin by
1104 the same move. =backup=, =gc= and the backfills stay host-local. #72
1105- =audit= filters: =--actor <user>= or =--actor -= for actorless rows,
1106 =--action <prefix>=, and =--since= as a duration (=30m=, =24h=, =7d=)
1107 or a date. =gitbayd admin audit= takes the same flags and =--json=. #73
1108- =dashboard= carries a =queues= block for admins: per worker —
1109 webhooks, mail, mirrors, builds, dependency checks — the pending,
1110 retrying and dead-lettered counts, the oldest pending age, and the
1111 retrying or failed rows themselves, capped at twenty each. The mail
1112 queue was readable nowhere before. =/admin= renders the block by
1113 dispatching =dashboard=; non-admins get a 404 and no rail link. #74
1114- =gitbayd admin config show= prints the configuration in effect as
1115 TOML, defaults filled in and =smtp_pass= redacted, so a support
1116 question starts from what runs rather than what was written. #81
1117- Build badges are served as PNG at =badge/build.png= as well as SVG,
1118 for places that will not render SVG.
1119- Host monitoring writes its reading to journald on every run and exits
1120 non-zero on an alert, so an unset webhook no longer looks like a
1121 healthy host; it reads the ACME cache where it actually lives and
1122 reports the soonest expiry. govulncheck runs as its own CI job. The
1123 database is snapshotted hourly by =gitbay-db-backup.timer=, keeping 48,
1124 alongside the nightly full archive; archives inherit the database's
1125 mode rather than the umask default. #28
1126- A runner whose log stream drops no longer fails the build it was
1127 recording. #67
1128- =go.yaml.in/yaml/v3= and =golang.org/x/net= bumped. #60
1129
1130Replace the binary and restart. No migration. The =gitbay-db-backup=
1131timer and the monitor changes ship in =deploy/cloud-init.yaml= for new
1132hosts; an existing host takes them from there by hand.
1133
1134* v1.3.0 — 2026-08-31
1135
1136A runner took every repository's work, which decided where you could run
1137one.
1138
1139- =runner next= takes optional =owner/name= arguments and =gitbay-runner=
1140 takes =-repos=, so a runner claims builds only for the repositories it
1141 names. Naming none is the old behaviour, so an existing runner is
1142 unaffected. This is what makes a runner outside the server practical: one
1143 on a machine that should build a single project no longer picks up a
1144 build belonging to someone else, which with open registration need not be
1145 anyone the operator knows. The scoping is what the runner asks for rather
1146 than an ACL the server holds over it — a runner account is admin by
1147 necessity, so the boundary is the operator choosing how to start it. #66
1148
1149Replace the binary and restart. Runners want the new binary too, though an
1150old one keeps working unscoped.
1151
1152* v1.2.1 — 2026-08-31
1153
1154Merging landed commits without running anything.
1155
1156- A merge moves the target ref with a direct ref update, so it never
1157 reached post-receive and none of the ref-update work fired: no =push=
1158 event for webhooks to subscribe to, and no builds. Pushing a commit to a
1159 branch ran the whole =.gitbay/ci.yml=; merging the identical commit ran
1160 nothing. Both now happen in the merge path, with the build queueing
1161 shared between it and post-receive rather than living in one of them.
1162 #65
1163
1164Replace the binary and restart.
1165
1166* v1.2.0 — 2026-08-31
1167
1168A merge request said what its state was and almost nothing about when it
1169got there. It now keeps time.
1170
1171- Reviews and checks carry the moment they last said something, on the
1172 merge request page and in =mr show=. A =ci/<job>= check also reports how
1173 long its build ran, so the timing of a run is readable without opening
1174 it. Checks posted through =status set= have no build and report only the
1175 time. !137
1176- A merged or closed merge request names who resolved it and when. The
1177 header stated "X wants to merge" whatever the state; it now says "X
1178 merged A into main on <date>", or "closed this without merging", and
1179 drops the claim entirely for a row that carries no stamp rather than
1180 inventing a time.
1181- =mr show= gains =merged_at=/=merged_by= and =closed_at=/=closed_by=, a
1182 =created_at= per review, and =updated_at= plus =duration= per check, in
1183 both JSON and text. Additive: nothing existing changed shape.
1184- The iOS client renders all of it as of krz/gitbay-ios!37.
1185
1186Replace the binary and restart. Migration 0029 adds four columns to
1187=merge_requests= and backfills the merge stamp for merge requests already
1188merged, reading the =mr.merged= events. A merge with no such event to read
1189— an import, or a row older than its events — keeps no stamp, and every
1190surface says nothing about when it happened rather than guessing.
1191
1192* v1.1.0 — 2026-08-31
1193
1194Every release until now answered questions. This one notices something
1195on its own: that a repository's dependencies have moved, and says so
1196without being asked.
1197
1198- Dependency update checks, opt-in per repository. A daemon worker reads
1199 the manifests on the default branch, asks the ecosystem's registry what
1200 the current release is, and maintains one issue per repository — opened
1201 when something falls behind, rewritten when the set changes, closed once
1202 nothing is behind. No package manager runs, so it needs no CI runner and
1203 no configuration beyond turning it on. #58
1204 - =go.mod= against proxy.golang.org, =package.json= with
1205 =package-lock.json= against npm, =Cargo.toml= with =Cargo.lock=
1206 against crates.io, =requirements.txt= and =pyproject.toml= against
1207 PyPI. A lockfile wins where both exist. A requirement naming a set
1208 rather than a release — a range, a wildcard, a git or path source — is
1209 skipped, since a range that already admits the newest release is not
1210 news.
1211 - =repo deps enable|disable|status=, a toggle on the repository settings
1212 page, and the same three over the JSON API.
1213 - Off by default, because checking a private repository tells a public
1214 registry what it depends on. That is the owner's disclosure to make.
1215 =[deps] check_interval_hours= sets the cadence, default 24.
1216 - The issue is authored by a =gitbay-bot= account, so the existing
1217 notification mail reaches the repository's owner rather than excluding
1218 them as the actor. Its table is padded to its columns, which a
1219 renderer ignores and a mail client needs. #61
1220- =help= takes a prefix — =help mr= narrows the registry to one noun — and
1221 every command now documents its arguments, so the server answers "what
1222 are the flags" instead of being guessed at. #57
1223- Web: settings rows end in one column of controls, checkboxes drawn at
1224 the height of the inputs and buttons they sit beside #59; an added line
1225 keeps its leading dash in the diff; a contributor's several verified
1226 addresses collapse to one row #56.
1227
1228Replace the binary and restart. Migration 0028 adds two tables and, when
1229the name is free, the =gitbay-bot= account. An instance already using that
1230name for a user or an org keeps what it has: the account is not created,
1231and dependency checks report that as the reason they cannot open an issue
1232rather than the migration failing and the daemon not starting. #64
1233
1234* v1.0.1 — 2026-08-30
1235
1236Two CI fixes found by running orgo's release pipeline on this instance.
1237
1238- A runner killed between claiming a build and reporting it left the build
1239 =running= forever, and the commit's =ci/<job>= status pending with it.
1240 =runner next= now fails builds past a 90-minute deadline — longer than the
1241 runner's own =-timeout= — and resolves their commit status. #53
1242- =gitbay keys add= and =gitbay repo deploy-key add= take stdin as a bare
1243 =< key.pub=, but only forwarded it when =--file -= appeared in the
1244 arguments, so both sent an empty body and rejected input the SSH API
1245 accepts. #55
1246
1247Replace the binary and restart.
1248
1249* v1.0.0 — 2026-08-26
1250
1251The web finishes the job #35 set it: reading, reviewing and responding,
1252with the CLI still the complete interface. Every capability exists over
1253SSH, every web write dispatches the same control command, and anything
1254whose input is a credential stays on the command line. The [[https://gitbay.org/krz/gitbay/wiki/Parity][Parity]] page in
1255the wiki is the maintained matrix and says which rows are deliberately
1256CLI-only.
1257
1258- Diff view: one foldable section per file with line-number gutters,
1259 per-file stats, rename and binary handling, and syntax highlighting
1260 run per hunk per side so multi-line constructs lex as real code.
1261 Commit and merge request pages share it; review threads anchor
1262 inline.
1263- Repository facts on the code page: commit, branch and tag counts,
1264 detected license, latest release, build status, a language census by
1265 tracked bytes, and contributors resolved to accounts by verified
1266 email. All derived from git at render time.
1267- Account settings on the web: SSH keys with scope, OpenPGP keys, and
1268 email addresses. Public keys are the only credential-shaped input the
1269 web accepts — they are not secret, and a new user needs one
1270 registered before the CLI is reachable to them. Token minting and
1271 account export stay SSH-only.
1272- Organizations are run from their page: membership and roles, teams,
1273 team members, and team repository grants, for org admins.
1274- Commit log shows each commit's combined check status and gains a path
1275 filter, so per-file history is reachable without editing the URL.
1276- Issue references from commit messages read "referenced in commit
1277 <sha> by <author>", linking the author when their email is verified
1278 here.
1279- The rail's focus ring uses the shell's own mark: the light scheme's
1280 accent was a dark blue ring on a black rail.
1281
1282No migrations. No new config.
1283
1284Upgrading from v0.5.0 is replace-the-binary-and-restart.
1285
1286* v0.5.0 — 2026-08-26
1287
1288A design pass and a parity pass. The web stops being a read-only
1289mirror of the CLI without becoming the place you are expected to work.
1290
1291- New design: a black shell with a persistent left rail carrying
1292 cross-repo state (pinned repos, review queue), the repo header
1293 rendered identically on every tab so navigation never moves, sharp
1294 lines, self-hosted IBM Plex, and code blocks that read against both
1295 color schemes. Tree listings sort directories first and carry each
1296 file's last commit; diffs, inputs and controls were reworked to
1297 match.
1298- Web parity with the CLI, dispatched through the same command
1299 registry the CLI and JSON API use — every web write is the
1300 equivalent =gitbay= command with =--source web=:
1301 - merge requests: review, resolve threads, merge, close, and open a
1302 new MR from the browser
1303 - issues: state, labels, assignees, milestones
1304 - repositories: settings, branch protection, visibility
1305 - releases: create and edit; builds: trigger a job
1306 Commands marked SSH-only still refuse over the web: build secrets,
1307 mirror tokens, domain claims, token minting, deletion and transfer.
1308- A dashboard that answers "what needs me": review queue, assigned
1309 issues, pinned repos, and an activity feed. Author names resolve to
1310 accounts and link to profiles wherever commits appear.
1311- Build status badges at =/{owner}/{repo}/badge/build.svg= for public
1312 repos.
1313- =[web] title= sets the instance's display name, separate from the
1314 hostname commands are pasted with.
1315- =make build/test/deploy= targets.
1316- Fixes: the mobile tab strip scrolls sideways only, long commit
1317 subjects no longer overflow on narrow screens, the shell fills the
1318 viewport with the line-length cap moved onto prose, and the account
1319 cell lines up with the page footer.
1320
1321No migrations. New config: =[web] title=.
1322
1323* v0.4.0 — 2026-08-25
1324
1325- Git LFS: standard clients work over both transports.
1326 =git-lfs-authenticate= joins the SSH dispatcher (deploy keys
1327 included; download needs read, upload write), minting stateless
1328 repo- and operation-scoped tokens for the batch API and basic
1329 transfers. Anonymous HTTPS downloads for public repos; uploads
1330 verified against size and sha256 before landing. Storage is
1331 content-addressed under =[lfs] root= behind a small interface an
1332 S3-compatible backend can drop into; =[lfs] max_object_bytes= caps
1333 objects (512MB default).
1334- Build failures mail the repo's notify targets with the log tail and
1335 build link — failed scheduled jobs reach an inbox.
1336- =release edit= updates a release's title and notes (absent flags
1337 keep their field); omaha-style CI note rebuilds work again.
1338- Syntax highlighting follows the color scheme: class-based chroma
1339 with light and dark palettes, the light-pinned code background is
1340 gone, and markdown fences and org src blocks highlight too. The UGC
1341 sanitizer admits only chroma's token-code classes.
1342
1343No migrations. New config: =[lfs] root=, =[lfs] max_object_bytes=.
1344
1345* v0.3.0 — 2026-08-25
1346
1347- CI: =.gitbay/ci.yml= jobs run as builds claimed by =gitbay-runner=
1348 over SSH (admin-only runner protocol; statuses feed =require-checks=).
1349 Per-repo secrets set over stdin and injected into build environments;
1350 cron schedules (server-local time) and tag-glob triggers, mutually
1351 exclusive per job; =build list/show/log/trigger= and a builds tab.
1352- Pages: public repos' =pages= branches served on =<owner>.<domain>=
1353 (=[pages] domain=), custom domains with DNS TXT ownership challenges
1354 and 7-day expiry for pending claims, per-subdomain on-demand ACME.
1355- Wikis (=.wiki= companion repos, access mirrors the parent) and teams
1356 within orgs (members-role scoping, per-repo team grants).
1357- Activity graphs on user and org pages, backfillable
1358 (=admin backfill-activity=); commits attributed by verified author
1359 email, deduped by sha.
1360- MR pages list the commits the MR carries; =mr show= gains a commits
1361 section.
1362- Per-file history: =?path== on the web log, =--path= on =repo log=,
1363 history link on blob pages.
1364- Mirror status surfaced in =repo show= and the repo header
1365 (admin-only), with sync errors visible; tag-only pushes now schedule
1366 mirror syncs.
1367- Rendering: GFM tables/strikethrough/autolinks/task lists, blob image
1368 previews, raw serves images with real content types (README images
1369 render under nosniff), 0BSD license recognition, repo website links,
1370 compact dashboard pins.
1371- =admin user delete= for accounts that anchor nothing, with named
1372 blockers otherwise.
1373- Fixes: wiki pages no longer overflow on mobile (iOS font-inflation
1374 trigger), GHSA-free deps, secret values pipe correctly through the
1375 CLI.
1376
1377Migrations 0020-0025 apply on start. New config: =[pages] domain=.
1378The runner is a new binary (=gitbay-runner=); see the wiki's Admin
1379guide for setup. Stress-tested against an import of git.git (82k
1380commits): see the wiki's Performance page.
1381
1382* v0.2.0 — 2026-08-24
1383
1384- Deploy keys: repo-bound CI keys (=repo deploy-key=), ro/rw, rename- and
1385 transfer-proof.
1386- Commit statuses (=status set/list=), combined state on MR pages, and a
1387 =require-checks= merge gate.
1388- Email notifications for issue and MR activity (participants with
1389 verified addresses; never the actor).
1390- Inline review threads on MR diffs (=mr diff-comment/threads/resolve=),
1391 stale on force-push, =require-resolved= merge gate.
1392- Required approvals with CODEOWNERS (=require-approvals=; latest review
1393 wins, author excluded) and a =require-resolved= gate; merge gate order
1394 is checks → approvals → CODEOWNERS → resolved threads → signatures.
1395- Web design revamp: token-based stylesheet (light+dark), wordmark and
1396 favicon, aligned layout grid, card-based listings, designed 404,
1397 mobile pass.
1398- Cross-references and mentions: =#N=, =!N=, =owner/name#N=, =@user=
1399 autolink in issue/MR text, viewer-aware for private repos.
1400- Archived repositories (read-only with badge) and repo topics.
1401- Blame view with signature-aware attribution and 1000-line pages.
1402- Repository search (name/description/topic) and per-repo code search
1403 (=repo grep=, web search tab); blob line anchors.
1404- Homepage: dashboard for logged-in users (pinned repos via =repo pin=,
1405 open MRs and issues involving you), landing page for visitors, full
1406 public listing at =/explore=; MR diffs collapsed by default.
1407- Milestones (=milestone create/list/close=, =issue/mr milestone=) with
1408 web progress; issue templates from =.gitbay/issue-template*.md=
1409 (CLI =$EDITOR= prefill and web form).
1410- Issue actions from commit messages landing on the default branch:
1411 =closes/fixes/resolves #N= closes, bare =#N= leaves a reference
1412 comment; once per issue+commit.
1413- Vanity Go imports: =[go_import]= config serves go-import meta tags, so
1414 =go install gitbay.org/gitbay/cmd/...@latest= works.
1415- Release script: =deploy/release.sh <tag>= builds reproducible
1416 linux/amd64, linux/arm64, and darwin/arm64 binaries with SHA256SUMS.
1417- Repository maintenance: =admin gc= (repack/prune, per-repo sizes),
1418 =admin stats= (counts + disk usage), weekly =gitbay-gc.timer=.
1419- Releases: tag-anchored notes and binary assets (=release= commands,
1420 assets over SSH stdin/stdout, web releases tab with downloads).
1421- GitHub history import: =repo import-issues= brings issues and PRs
1422 (as merged/closed MRs) with comments, labels, and state, attributed
1423 inline and resumable.
1424- Push and pull mirroring (=repo mirror=): background sync, read-only
1425 pull mirrors, per-mirror status; credentials server-side, SSRF-guarded.
1426- Web signup at =/register= for open and invite instances.
1427- Audit log (=audit=, =gitbayd admin audit=): every mutating command
1428 with source key fingerprint, registrations, force-pushes, auth
1429 failures. Hardening: per-IP auth-failure throttling
1430 (=ssh_auth_rate=), =max_pack_bytes= enforced, =admin user
1431 disable/enable=.
1432- Account migration: =gitbay migrate --from <host>= (bundle
1433 export/replay + client-side git mirror); =gitbay auth export= as a
1434 user-level backup.
1435- Editable issues and MRs (=issue edit=, =mr edit=, web forms).
1436- Commit references appear as system messages with linked shas.
1437- Design: top nav, repo listing rows (topics, license, last updated),
1438 file table headers, README relative-link resolution, branch
1439 dropdown, web pinning, org owner picker, label filters, linked
1440 usernames and commit parents, =/privacy= page, blue accent.
1441
1442Upgrade notes: migrations 0006–0019 apply on start. No config changes
1443required; =[go_import]=, =[mirrors]=, =web.privacy_notice=, and
1444=web.mode = "accounts"= are opt-in.
1445
1446* v0.1.0 — 2026-08-24
1447
1448First tagged release: the complete CLI-first forge. SSH control plane
1449(bare-OpenSSH usable), git over SSH/HTTPS/git-daemon, repos, issues,
1450merge requests (ff/merge/squash/rebase with signature policy), OpenPGP
1451and SSHSIG verification with retroactive re-verification, orgs, repo
1452import, web UI (view-only or accounts mode), registration with invites
1453and SMTP, HTTPS/JSON API with SSH-minted tokens, webhooks, backups,
1454ACME TLS, systemd deployment.