.gitbay/wiki/API.org

3e75710732680bc79357080d72a716a869571646
gitbay/.gitbay/wiki/API.org rendered · source · history · blame · raw

186 lines · 7041 bytes

gitbay API and webhooks

The JSON API

One endpoint fronts the entire command registry — everything the SSH control plane can do, current and future, with identical semantics. Disabled by default; the instance must set:

[api]
enabled = true

Tokens

Tokens are minted over SSH and only over SSH — an API token can never create further credentials.

gitbay auth token create --name ci [--scope full|read] [--ttl 30d]
gitbay auth token list
gitbay auth token revoke ci

The token (prefix gb_, shown exactly once) is presented as Authorization: Bearer gb_.... Only a hash is stored server-side. Scope read permits list/show/log/diff-style commands and refuses anything that modifies state. --ttl takes Go durations or a day suffix (30d); expired, revoked, and unknown tokens all answer the same 401.

POST /api/v1/cmd

Request body:

{"argv": ["issue", "create", "you/project", "--title", "from CI"],
 "stdin": "optional body for --file - style input"}

argv is real argv — no shell, no quoting rules. The response is the command's own JSON envelope with exit_code added, plus stderr when the command wrote diagnostics:

{"protocol_version": 1, "exit_code": 0, "data": {"number": 7}}

HTTP status maps the exit code: 0→200, 2→400, 3→404, 4→403, else 500. Commands that emit raw text rather than an envelope (help, mr diff) come wrapped as {"output": "..."}. Git transport commands and the token commands are refused by name.

curl -s -H "Authorization: Bearer $TOKEN" \
     -d '{"argv":["whoami"]}' https://gitbay.org/api/v1/cmd

printf '{"argv":["issue","create","you/project","--title","t","--file","-"],"stdin":"body\n"}' |
curl -s -H "Authorization: Bearer $TOKEN" -d @- https://gitbay.org/api/v1/cmd

GET /api/v1/read

The conditional half: the same registry over GET, admitting only commands the registry marks read-only, so a GET structurally cannot mutate. Responses carry an ETag; If-None-Match answers 304.

curl -s -H "Authorization: Bearer $TOKEN" \
     "https://gitbay.org/api/v1/read?argv=repo&argv=show&argv=you/project"

The dashboard read

dashboard returns the account aggregate — pinned repositories, open merge requests, assigned issues, recent builds — in one call:

curl -s -H "Authorization: Bearer $TOKEN" \
     "https://gitbay.org/api/v1/read?argv=dashboard"

For an instance admin the response also carries {"server": {"commit": "<sha>"}}, the build the daemon is running, so the deployed commit is readable without the journal. The key is absent for everyone else: the exact build a host runs narrows down which known issues apply to it.

Cursor pagination

issue list, mr list, repo list, and feed accept --limit <n> (1–200) and --cursor <c>. With either flag present the data envelope becomes {"items": [...], "next": "..."}; next is an opaque cursor for the following page, absent on the last one. Without the flags the response stays the complete bare array; a bare feed returns the newest 50 events.

Commit statuses (CI reporting)

CI reports results through the same command surface (over SSH or the JSON API with a full-scope token; reporting requires write access):

gitbay status set <owner/name> <sha> --context build --state pending
gitbay status set <owner/name> <sha> --context build --state success --url https://ci.example/run/1
gitbay status list <owner/name> <sha> --json    # {"combined": "...", "statuses": [...]}

One row per (commit, context): re-reporting updates in place. States: pending, success, failure, error; the combined state is the worst of them. Statuses appear on commit pages, MR pages, and mr show, each with updated_at; a ci/<job> status also carries duration, read from the build behind it, once that build has finished. With repo settings require-checks <repo> on, merging requires the MR head to carry statuses and all of them green. Each report also emits a status event to webhooks.

Webhooks

Per-repository outbound POSTs for repository events. Managed by repo admins:

gitbay webhook add <url> --secret s3cret [--events push,issue.created]  # default *
gitbay webhook list
gitbay webhook deliveries [--limit 50]     # status, attempts, last error
gitbay webhook redeliver <delivery-id>     # requeue, including dead letters
gitbay webhook remove <id>

Events

Every event this forge records, and so every name --events may take. The server holds the same list as control.EventKinds, and a test fails if the code emits something not listed or lists something it never emits — so this is the whole set, not a sample. webhook add refuses a name that is not one of them, since a subscription to a typo would silently never fire.

  • repository: push (ref, old, new, forced, deleted), repo.archived, repo.unarchived, repo.imported (from)
  • issues: issue.created, issue.edited, issue.commented, issue.closed, issue.open, issue.labeled (labels), issue.assigned (assignees), issue.milestoned (milestone)
  • merge requests: mr.created, mr.edited, mr.commented, mr.reviewed (verdict), mr.draft (draft), mr.retargeted (from, to), mr.milestoned (milestone), mr.merged (number, sha), mr.closed
  • releases: release.created (tag), release.deleted (tag)
  • CI: status, build.success, build.failure, build.cancelled

Every payload carries number where it names an issue or merge request.

There is deliberately no repo.deleted. Both events.repo_id and webhooks.repo_id cascade from repos, so recording one would delete it — and every webhook that could have received it — in the same statement. A repository's deletion is visible in the audit log.

Delivery

Each event POSTs one JSON body:

{"event": "push",
 "repo": "you/project",
 "actor": "alice",
 "created_at": "2026-08-24T01:00:00.000Z",
 "data": {"ref": "refs/heads/main", "old": "...", "new": "...",
          "forced": false, "deleted": false}}

Headers: X-Gitbay-Event, X-Gitbay-Delivery (id), and — when the hook has a secret — X-Gitbay-Signature-256: sha256=<hex hmac> over the raw body. Verify before trusting:

import hmac, hashlib
expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers["X-Gitbay-Signature-256"])

A 2xx within 10 seconds is success. Anything else retries with exponential backoff (30s base, doubling) and dead-letters after five attempts; webhook deliveries shows the trail and redeliver revives a dead letter. Redirects are never followed, and targets resolving to loopback/private/link-local addresses are refused both at registration and again at connect time, unless the instance sets [webhooks] allow_local.