.gitbay/wiki/API.org
194 lines · 7625 bytes
1#+title: gitbay API and webhooks
2
3* The JSON API
4
5One endpoint fronts the entire command registry — everything the SSH
6control plane can do, current and future, with identical semantics.
7Disabled by default; the instance must set:
8
9#+begin_src toml
10[api]
11enabled = true
12#+end_src
13
14** Tokens
15
16Tokens are minted wherever the registry is reached: over SSH, on the
17API, anywhere. A full-scope token can mint another, which is what full
18scope means; a read-scoped one cannot, because minting is a write. The
19controls here are scope, TTL and revocation, not which door a request
20arrived through (#234). Give a token the narrowest scope and shortest
21TTL that does its job, and revoke it when the job is over.
22
23#+begin_src sh
24gitbay auth token create --name ci [--scope full|read] [--ttl 30d]
25gitbay auth token list
26gitbay auth token revoke ci
27#+end_src
28
29The token (prefix =gb_=, shown exactly once) is presented as
30=Authorization: Bearer gb_...=. Only a hash is stored server-side.
31Scope =read= permits list/show/log/diff-style commands and refuses
32anything that modifies state. =--ttl= takes Go durations or a day
33suffix (=30d=); expired, revoked, and unknown tokens all answer the
34same 401.
35
36** POST /api/v1/cmd
37
38Request body:
39#+begin_src json
40{"argv": ["issue", "create", "you/project", "--title", "from CI"],
41 "stdin": "optional body for --file - style input"}
42#+end_src
43
44=argv= is real argv — no shell, no quoting rules. The response is the
45command's own JSON envelope with =exit_code= added, plus =stderr= when
46the command wrote diagnostics:
47
48#+begin_src json
49{"protocol_version": 1, "exit_code": 0, "data": {"number": 7}}
50#+end_src
51
52HTTP status maps the exit code: 0→200, 2→400, 3→404, 4→403, else 500.
53Commands that emit raw text rather than an envelope (=help=, =mr diff=)
54come wrapped as ={"output": "..."}=. Git transport commands and the
55token commands are refused by name.
56
57#+begin_src sh
58curl -s -H "Authorization: Bearer $TOKEN" \
59 -d '{"argv":["whoami"]}' https://gitbay.org/api/v1/cmd
60
61printf '{"argv":["issue","create","you/project","--title","t","--file","-"],"stdin":"body\n"}' |
62curl -s -H "Authorization: Bearer $TOKEN" -d @- https://gitbay.org/api/v1/cmd
63#+end_src
64
65** GET /api/v1/read
66
67The conditional half: the same registry over GET, admitting only
68commands the registry marks read-only, so a GET structurally cannot
69mutate. Responses carry an =ETag=; =If-None-Match= answers 304.
70
71#+begin_src sh
72curl -s -H "Authorization: Bearer $TOKEN" \
73 "https://gitbay.org/api/v1/read?argv=repo&argv=show&argv=you/project"
74#+end_src
75
76** The dashboard read
77
78=dashboard= returns the account aggregate — pinned repositories, open
79merge requests, assigned issues, recent builds — in one call:
80
81#+begin_src sh
82curl -s -H "Authorization: Bearer $TOKEN" \
83 "https://gitbay.org/api/v1/read?argv=dashboard"
84#+end_src
85
86For an instance admin the response also carries ={"server": {"commit":
87"<sha>"}}=, the build the daemon is running, so the deployed commit is
88readable without the journal. The key is absent for everyone else: the
89exact build a host runs narrows down which known issues apply to it.
90
91** Cursor pagination
92
93=issue list=, =mr list=, =repo list=, and =feed= accept =--limit <n>=
94(1–200) and =--cursor <c>=. With either flag present the =data=
95envelope becomes ={"items": [...], "next": "..."}=; =next= is an
96opaque cursor for the following page, absent on the last one. Without
97the flags the response stays the complete bare array; a bare =feed=
98returns the newest 50 events.
99
100* Commit statuses (CI reporting)
101
102CI reports results through the same command surface (over SSH or the
103JSON API with a full-scope token; reporting requires write access):
104
105#+begin_src sh
106gitbay status set <owner/name> <sha> --context build --state pending
107gitbay status set <owner/name> <sha> --context build --state success --url https://ci.example/run/1
108gitbay status list <owner/name> <sha> --json # {"combined": "...", "statuses": [...]}
109#+end_src
110
111One row per (commit, context): re-reporting updates in place. States:
112=pending=, =success=, =failure=, =error=; the combined state is the
113worst of them. Statuses appear on commit pages, MR pages, and
114=mr show=, each with =updated_at=; a =ci/<job>= status also carries
115=duration=, read from the build behind it, once that build has
116finished. With =repo settings require-checks <repo> on=, every status
117on the MR head must be green, and a head something was going to report
118on must carry some: a =.gitbay/ci.yml= with a job a push runs, or a
119repository that has recorded a status before, which is what reporting
120from outside through =status set= looks like. A repository where
121nothing has ever reported merges. Each
122report also emits a =status= event to webhooks.
123
124* Webhooks
125
126Per-repository outbound POSTs for repository events. Managed by repo
127admins:
128
129#+begin_src sh
130gitbay webhook add <url> --secret s3cret [--events push,issue.created] # default *
131gitbay webhook list
132gitbay webhook deliveries [--limit 50] # status, attempts, last error
133gitbay webhook redeliver <delivery-id> # requeue, including dead letters
134gitbay webhook remove <id>
135#+end_src
136
137** Events
138
139Every event this forge records, and so every name =--events= may take.
140The server holds the same list as =control.EventKinds=, and a test
141fails if the code emits something not listed or lists something it never
142emits — so this is the whole set, not a sample. =webhook add= refuses a
143name that is not one of them, since a subscription to a typo would
144silently never fire.
145
146- repository: =push= (ref, old, new, forced, deleted), =repo.archived=,
147 =repo.unarchived=, =repo.imported= (from)
148- issues: =issue.created=, =issue.edited=, =issue.commented=,
149 =issue.closed=, =issue.open=, =issue.labeled= (labels),
150 =issue.assigned= (assignees), =issue.milestoned= (milestone)
151- merge requests: =mr.created=, =mr.edited=, =mr.commented=,
152 =mr.reviewed= (verdict), =mr.draft= (draft), =mr.retargeted= (from,
153 to), =mr.labeled= (labels), =mr.milestoned= (milestone), =mr.merged=
154 (number, sha), =mr.closed=
155- releases: =release.created= (tag), =release.deleted= (tag)
156- CI: =status=, =build.success=, =build.failure=, =build.cancelled=
157
158Every payload carries =number= where it names an issue or merge request.
159
160There is deliberately no =repo.deleted=. Both =events.repo_id= and
161=webhooks.repo_id= cascade from =repos=, so recording one would delete
162it — and every webhook that could have received it — in the same
163statement. A repository's deletion is visible in the audit log.
164
165** Delivery
166
167Each event POSTs one JSON body:
168
169#+begin_src json
170{"event": "push",
171 "repo": "you/project",
172 "actor": "alice",
173 "created_at": "2026-08-24T01:00:00.000Z",
174 "data": {"ref": "refs/heads/main", "old": "...", "new": "...",
175 "forced": false, "deleted": false}}
176#+end_src
177
178Headers: =X-Gitbay-Event=, =X-Gitbay-Delivery= (id), and — when the hook
179has a secret — =X-Gitbay-Signature-256: sha256=<hex hmac>= over the raw
180body. Verify before trusting:
181
182#+begin_src python
183import hmac, hashlib
184expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
185ok = hmac.compare_digest(expected, request.headers["X-Gitbay-Signature-256"])
186#+end_src
187
188A 2xx within 10 seconds is success. Anything else retries with
189exponential backoff (30s base, doubling) and dead-letters after five
190attempts; =webhook deliveries= shows the trail and =redeliver= revives a
191dead letter. Redirects are never followed, and targets resolving to
192loopback/private/link-local addresses are refused both at registration
193and again at connect time, unless the instance sets
194=[webhooks] allow_local=.